Hasta ahora, has visto cómo crear un servidor y un cliente. El cliente ha podido llamar explícitamente al servidor para listar sus herramientas, recursos y prompts. Sin embargo, no es un enfoque muy práctico. Tu usuario vive en la era de los agentes y espera usar prompts y comunicarse con un LLM para hacerlo. Para tu usuario, no importa si usas MCP o no para almacenar tus capacidades, pero sí espera interactuar usando lenguaje natural. Entonces, ¿cómo resolvemos esto? La solución consiste en agregar un LLM al cliente.
En esta lección nos enfocamos en agregar un LLM a tu cliente y mostramos cómo esto proporciona una experiencia mucho mejor para tu usuario.
Al final de esta lección, serás capaz de:
- Crear un cliente con un LLM.
- Interactuar sin problemas con un servidor MCP usando un LLM.
- Proporcionar una mejor experiencia al usuario final en el lado del cliente.
Intentemos entender el enfoque que necesitamos tomar. Agregar un LLM suena simple, pero ¿realmente lo haremos?
Así es como el cliente interactuará con el servidor:
-
Establecer conexión con el servidor.
-
Listar capacidades, prompts, recursos y herramientas, y guardar su esquema.
-
Agregar un LLM y pasar las capacidades guardadas y su esquema en un formato que el LLM entienda.
-
Manejar un prompt del usuario pasándolo al LLM junto con las herramientas listadas por el cliente.
Genial, ahora entendemos cómo podemos hacer esto a alto nivel, intentémoslo en el siguiente ejercicio.
En este ejercicio, aprenderemos a agregar un LLM a nuestro cliente.
Crear un token de GitHub es un proceso sencillo. Aquí te explicamos cómo hacerlo:
- Ve a Configuración de GitHub – Haz clic en tu foto de perfil en la esquina superior derecha y selecciona Configuración.
- Navega a Configuración de desarrollador – Desplázate hacia abajo y haz clic en Configuración de desarrollador.
- Selecciona Tokens de acceso personal – Haz clic en Tokens de acceso personal y luego en Generar nuevo token.
- Configura tu token – Agrega una nota para referencia, establece una fecha de expiración y selecciona los alcances necesarios (permisos).
- Genera y copia el token – Haz clic en Generar token y asegúrate de copiarlo inmediatamente, ya que no podrás verlo nuevamente.
Primero, creemos nuestro cliente:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import OpenAI from "openai";
import { z } from "zod"; // Import zod for schema validation
class MCPClient {
private openai: OpenAI;
private client: Client;
constructor(){
this.openai = new OpenAI({
baseURL: "https://models.inference.ai.azure.com",
apiKey: process.env.GITHUB_TOKEN,
});
this.client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
}
}En el código anterior hemos:
- Importado las bibliotecas necesarias.
- Creado una clase con dos miembros,
clientyopenai, que nos ayudarán a gestionar un cliente e interactuar con un LLM respectivamente. - Configurado nuestra instancia de LLM para usar GitHub Models estableciendo
baseUrlpara apuntar a la API de inferencia.
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client
# Create server parameters for stdio connection
server_params = StdioServerParameters(
command="mcp", # Executable
args=["run", "server.py"], # Optional command line arguments
env=None, # Optional environment variables
)
async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read, write
) as session:
# Initialize the connection
await session.initialize()
if __name__ == "__main__":
import asyncio
asyncio.run(run())En el código anterior hemos:
- Importado las bibliotecas necesarias para MCP.
- Creado un cliente.
using Azure;
using Azure.AI.Inference;
using Azure.Identity;
using System.Text.Json;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
using System.Text.Json;
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "/workspaces/mcp-for-beginners/03-GettingStarted/02-client/solution/server/bin/Debug/net8.0/server",
Arguments = [],
});
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport);Primero, necesitarás agregar las dependencias de LangChain4j a tu archivo pom.xml. Agrega estas dependencias para habilitar la integración de MCP y el soporte para GitHub Models:
<properties>
<langchain4j.version>1.0.0-beta3</langchain4j.version>
</properties>
<dependencies>
<!-- LangChain4j MCP Integration -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- OpenAI Official API Client -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-official</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- GitHub Models Support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-github-models</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- Spring Boot Starter (optional, for production apps) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>Luego, crea tu clase cliente en Java:
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.McpTransport;
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openaiofficial.OpenAiOfficialChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolProvider;
import java.time.Duration;
import java.util.List;
public class LangChain4jClient {
public static void main(String[] args) throws Exception { // Configure the LLM to use GitHub Models
ChatLanguageModel model = OpenAiOfficialChatModel.builder()
.isGitHubModels(true)
.apiKey(System.getenv("GITHUB_TOKEN"))
.timeout(Duration.ofSeconds(60))
.modelName("gpt-4.1-nano")
.build();
// Create MCP transport for connecting to server
McpTransport transport = new HttpMcpTransport.Builder()
.sseUrl("http://localhost:8080/sse")
.timeout(Duration.ofSeconds(60))
.logRequests(true)
.logResponses(true)
.build();
// Create MCP client
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
}
}En el código anterior hemos:
- Agregado dependencias de LangChain4j: Necesarias para la integración de MCP, el cliente oficial de OpenAI y el soporte para GitHub Models.
- Importado las bibliotecas de LangChain4j: Para la integración de MCP y la funcionalidad del modelo de chat de OpenAI.
- Creado un
ChatLanguageModel: Configurado para usar GitHub Models con tu token de GitHub. - Configurado transporte HTTP: Usando eventos enviados por el servidor (SSE) para conectarse al servidor MCP.
- Creado un cliente MCP: Que manejará la comunicación con el servidor.
- Usado el soporte MCP integrado de LangChain4j: Que simplifica la integración entre LLMs y servidores MCP.
Este ejemplo asume que tienes un servidor MCP basado en Rust en ejecución. Si no tienes uno, consulta la lección 01-primer-servidor para crear el servidor.
Una vez que tengas tu servidor MCP en Rust, abre una terminal y navega al mismo directorio que el servidor. Luego ejecuta el siguiente comando para crear un nuevo proyecto de cliente LLM:
mkdir calculator-llmclient
cd calculator-llmclient
cargo initAgrega las siguientes dependencias a tu archivo Cargo.toml:
[dependencies]
async-openai = { version = "0.29.0", features = ["byot"] }
rmcp = { version = "0.5.0", features = ["client", "transport-child-process"] }
serde_json = "1.0.141"
tokio = { version = "1.46.1", features = ["rt-multi-thread"] }Note
No existe una biblioteca oficial de Rust para OpenAI, sin embargo, el crate async-openai es una biblioteca mantenida por la comunidad que se usa comúnmente.
Abre el archivo src/main.rs y reemplaza su contenido con el siguiente código:
use async_openai::{Client, config::OpenAIConfig};
use rmcp::{
RmcpError,
model::{CallToolRequestParam, ListToolsResult},
service::{RoleClient, RunningService, ServiceExt},
transport::{ConfigureCommandExt, TokioChildProcess},
};
use serde_json::{Value, json};
use std::error::Error;
use tokio::process::Command;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Initial message
let mut messages = vec![json!({"role": "user", "content": "What is the sum of 3 and 2?"})];
// Setup OpenAI client
let api_key = std::env::var("OPENAI_API_KEY")?;
let openai_client = Client::with_config(
OpenAIConfig::new()
.with_api_base("https://models.github.ai/inference/chat")
.with_api_key(api_key),
);
// Setup MCP client
let server_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("calculator-server");
let mcp_client = ()
.serve(
TokioChildProcess::new(Command::new("cargo").configure(|cmd| {
cmd.arg("run").current_dir(server_dir);
}))
.map_err(RmcpError::transport_creation::<TokioChildProcess>)?,
)
.await?;
// TODO: Get MCP tool listing
// TODO: LLM conversation with tool calls
Ok(())
}Este código configura una aplicación básica en Rust que se conectará a un servidor MCP y GitHub Models para interacciones con LLM.
Important
Asegúrate de establecer la variable de entorno OPENAI_API_KEY con tu token de GitHub antes de ejecutar la aplicación.
Genial, para nuestro próximo paso, listemos las capacidades en el servidor.
Ahora nos conectaremos al servidor y pediremos sus capacidades:
En la misma clase, agrega los siguientes métodos:
async connectToServer(transport: Transport) {
await this.client.connect(transport);
this.run();
console.error("MCPClient started on stdin/stdout");
}
async run() {
console.log("Asking server for available tools");
// listing tools
const toolsResult = await this.client.listTools();
}En el código anterior hemos:
- Agregado código para conectarse al servidor,
connectToServer. - Creado un método
runresponsable de manejar el flujo de nuestra aplicación. Hasta ahora solo lista las herramientas, pero pronto agregaremos más.
# List available resources
resources = await session.list_resources()
print("LISTING RESOURCES")
for resource in resources:
print("Resource: ", resource)
# List available tools
tools = await session.list_tools()
print("LISTING TOOLS")
for tool in tools.tools:
print("Tool: ", tool.name)
print("Tool", tool.inputSchema["properties"])Esto es lo que hemos agregado:
- Listado de recursos y herramientas y los hemos impreso. Para las herramientas también listamos
inputSchema, que usaremos más adelante.
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
Console.WriteLine($"Tool description: {tool.Description}");
Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
// TODO: convert tool definition from MCP tool to LLm tool
}
return toolDefinitions;
}En el código anterior hemos:
- Listado las herramientas disponibles en el servidor MCP.
- Para cada herramienta, listado nombre, descripción y su esquema. Este último es algo que usaremos para llamar a las herramientas próximamente.
// Create a tool provider that automatically discovers MCP tools
ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
// The MCP tool provider automatically handles:
// - Listing available tools from the MCP server
// - Converting MCP tool schemas to LangChain4j format
// - Managing tool execution and responsesEn el código anterior hemos:
- Creado un
McpToolProviderque descubre y registra automáticamente todas las herramientas del servidor MCP. - El proveedor de herramientas maneja la conversión entre esquemas de herramientas MCP y el formato de herramientas de LangChain4j internamente.
- Este enfoque abstrae el proceso manual de listado y conversión de herramientas.
Recuperar herramientas del servidor MCP se realiza usando el método list_tools. En tu función main, después de configurar el cliente MCP, agrega el siguiente código:
// Get MCP tool listing
let tools = mcp_client.list_tools(Default::default()).await?;El siguiente paso después de listar las capacidades del servidor es convertirlas en un formato que el LLM entienda. Una vez que hagamos eso, podemos proporcionar estas capacidades como herramientas a nuestro LLM.
-
Agrega el siguiente código para convertir la respuesta del servidor MCP a un formato de herramienta que el LLM pueda usar:
openAiToolAdapter(tool: { name: string; description?: string; input_schema: any; }) { // Create a zod schema based on the input_schema const schema = z.object(tool.input_schema); return { type: "function" as const, // Explicitly set type to "function" function: { name: tool.name, description: tool.description, parameters: { type: "object", properties: tool.input_schema.properties, required: tool.input_schema.required, }, }, }; }
El código anterior toma una respuesta del servidor MCP y la convierte en un formato de definición de herramienta que el LLM pueda entender.
-
Actualicemos el método
runpara listar las capacidades del servidor:async run() { console.log("Asking server for available tools"); const toolsResult = await this.client.listTools(); const tools = toolsResult.tools.map((tool) => { return this.openAiToolAdapter({ name: tool.name, description: tool.description, input_schema: tool.inputSchema, }); }); }
En el código anterior, hemos actualizado el método
runpara recorrer el resultado y para cada entrada llamar aopenAiToolAdapter.
-
Primero, creemos la siguiente función de conversión:
def convert_to_llm_tool(tool): tool_schema = { "type": "function", "function": { "name": tool.name, "description": tool.description, "type": "function", "parameters": { "type": "object", "properties": tool.inputSchema["properties"] } } } return tool_schema
En la función anterior
convert_to_llm_toolstomamos una respuesta de herramienta MCP y la convertimos a un formato que el LLM pueda entender. -
Luego, actualicemos nuestro código de cliente para aprovechar esta función así:
for tool in tools.tools: print("Tool: ", tool.name) print("Tool", tool.inputSchema["properties"]) functions.append(convert_to_llm_tool(tool))
Aquí, estamos agregando una llamada a
convert_to_llm_toolpara convertir la respuesta de herramienta MCP en algo que podamos alimentar al LLM más adelante.
- Agreguemos código para convertir la respuesta de herramienta MCP en algo que el LLM pueda entender:
ChatCompletionsToolDefinition ConvertFrom(string name, string description, JsonElement jsonElement)
{
// convert the tool to a function definition
FunctionDefinition functionDefinition = new FunctionDefinition(name)
{
Description = description,
Parameters = BinaryData.FromObjectAsJson(new
{
Type = "object",
Properties = jsonElement
},
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
};
// create a tool definition
ChatCompletionsToolDefinition toolDefinition = new ChatCompletionsToolDefinition(functionDefinition);
return toolDefinition;
}En el código anterior hemos:
- Creado una función
ConvertFromque toma nombre, descripción y esquema de entrada. - Definido funcionalidad que crea una FunctionDefinition que se pasa a un ChatCompletionsDefinition. Este último es algo que el LLM puede entender.
-
Veamos cómo podemos actualizar algo de código existente para aprovechar esta función anterior:
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools() { Console.WriteLine("Listing tools"); var tools = await mcpClient.ListToolsAsync(); List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>(); foreach (var tool in tools) { Console.WriteLine($"Connected to server with tools: {tool.Name}"); Console.WriteLine($"Tool description: {tool.Description}"); Console.WriteLine($"Tool parameters: {tool.JsonSchema}"); JsonElement propertiesElement; tool.JsonSchema.TryGetProperty("properties", out propertiesElement); var def = ConvertFrom(tool.Name, tool.Description, propertiesElement); Console.WriteLine($"Tool definition: {def}"); toolDefinitions.Add(def); Console.WriteLine($"Properties: {propertiesElement}"); } return toolDefinitions; }
En el código anterior hemos:
-
Actualizado la función para convertir la respuesta de herramienta MCP en una herramienta LLM. Destaquemos el código que agregamos:
JsonElement propertiesElement; tool.JsonSchema.TryGetProperty("properties", out propertiesElement); var def = ConvertFrom(tool.Name, tool.Description, propertiesElement); Console.WriteLine($"Tool definition: {def}"); toolDefinitions.Add(def);
El esquema de entrada es parte de la respuesta de herramienta pero en el atributo "properties", por lo que necesitamos extraerlo. Además, ahora llamamos a
ConvertFromcon los detalles de la herramienta. Ahora que hemos hecho el trabajo pesado, veamos cómo todo se junta mientras manejamos un prompt del usuario a continuación.
-
// Create a Bot interface for natural language interaction
public interface Bot {
String chat(String prompt);
}
// Configure the AI service with LLM and MCP tools
Bot bot = AiServices.builder(Bot.class)
.chatLanguageModel(model)
.toolProvider(toolProvider)
.build();En el código anterior hemos:
- Definido una interfaz simple
Botpara interacciones en lenguaje natural. - Usado los
AiServicesde LangChain4j para vincular automáticamente el LLM con el proveedor de herramientas MCP. - El marco maneja automáticamente la conversión de esquemas de herramientas MCP y la llamada a funciones detrás de escena.
- Este enfoque elimina la conversión manual de herramientas: LangChain4j maneja toda la complejidad de convertir herramientas MCP a un formato compatible con LLM.
Para convertir la respuesta de herramienta MCP a un formato que el LLM pueda entender, agregaremos una función auxiliar que formatea el listado de herramientas. Agrega el siguiente código a tu archivo main.rs debajo de la función main. Esto se llamará al hacer solicitudes al LLM:
async fn format_tools(tools: &ListToolsResult) -> Result<Vec<Value>, Box<dyn Error>> {
let tools_json = serde_json::to_value(tools)?;
let Some(tools_array) = tools_json.get("tools").and_then(|t| t.as_array()) else {
return Ok(vec![]);
};
let formatted_tools = tools_array
.iter()
.filter_map(|tool| {
let name = tool.get("name")?.as_str()?;
let description = tool.get("description")?.as_str()?;
let schema = tool.get("inputSchema")?;
Some(json!({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": {
"type": "object",
"properties": schema.get("properties").unwrap_or(&json!({})),
"required": schema.get("required").unwrap_or(&json!([]))
}
}
}))
})
.collect();
Ok(formatted_tools)
}Genial, ahora estamos listos para manejar cualquier solicitud del usuario, así que abordemos eso a continuación.
En esta parte del código, manejaremos las solicitudes del usuario.
-
Agrega un método que se usará para llamar a nuestro LLM:
async callTools( tool_calls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[], toolResults: any[] ) { for (const tool_call of tool_calls) { const toolName = tool_call.function.name; const args = tool_call.function.arguments; console.log(`Calling tool ${toolName} with args ${JSON.stringify(args)}`); // 2. Call the server's tool const toolResult = await this.client.callTool({ name: toolName, arguments: JSON.parse(args), }); console.log("Tool result: ", toolResult); // 3. Do something with the result // TODO } }
En el código anterior hemos:
-
Agregado un método
callTools. -
El método toma una respuesta del LLM y verifica qué herramientas se han llamado, si es que hay alguna:
for (const tool_call of tool_calls) { const toolName = tool_call.function.name; const args = tool_call.function.arguments; console.log(`Calling tool ${toolName} with args ${JSON.stringify(args)}`); // call tool }
-
Llama a una herramienta, si el LLM indica que debe ser llamada:
// 2. Call the server's tool const toolResult = await this.client.callTool({ name: toolName, arguments: JSON.parse(args), }); console.log("Tool result: ", toolResult); // 3. Do something with the result // TODO
-
-
Actualiza el método
runpara incluir llamadas al LLM y llamar acallTools:// 1. Create messages that's input for the LLM const prompt = "What is the sum of 2 and 3?" const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ { role: "user", content: prompt, }, ]; console.log("Querying LLM: ", messages[0].content); // 2. Calling the LLM let response = this.openai.chat.completions.create({ model: "gpt-4o-mini", max_tokens: 1000, messages, tools: tools, }); let results: any[] = []; // 3. Go through the LLM response,for each choice, check if it has tool calls (await response).choices.map(async (choice: { message: any; }) => { const message = choice.message; if (message.tool_calls) { console.log("Making tool call") await this.callTools(message.tool_calls, results); } });
Genial, listemos el código completo:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import OpenAI from "openai";
import { z } from "zod"; // Import zod for schema validation
class MyClient {
private openai: OpenAI;
private client: Client;
constructor(){
this.openai = new OpenAI({
baseURL: "https://models.inference.ai.azure.com", // might need to change to this url in the future: https://models.github.ai/inference
apiKey: process.env.GITHUB_TOKEN,
});
this.client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
}
async connectToServer(transport: Transport) {
await this.client.connect(transport);
this.run();
console.error("MCPClient started on stdin/stdout");
}
openAiToolAdapter(tool: {
name: string;
description?: string;
input_schema: any;
}) {
// Create a zod schema based on the input_schema
const schema = z.object(tool.input_schema);
return {
type: "function" as const, // Explicitly set type to "function"
function: {
name: tool.name,
description: tool.description,
parameters: {
type: "object",
properties: tool.input_schema.properties,
required: tool.input_schema.required,
},
},
};
}
async callTools(
tool_calls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[],
toolResults: any[]
) {
for (const tool_call of tool_calls) {
const toolName = tool_call.function.name;
const args = tool_call.function.arguments;
console.log(`Calling tool ${toolName} with args ${JSON.stringify(args)}`);
// 2. Call the server's tool
const toolResult = await this.client.callTool({
name: toolName,
arguments: JSON.parse(args),
});
console.log("Tool result: ", toolResult);
// 3. Do something with the result
// TODO
}
}
async run() {
console.log("Asking server for available tools");
const toolsResult = await this.client.listTools();
const tools = toolsResult.tools.map((tool) => {
return this.openAiToolAdapter({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
});
});
const prompt = "What is the sum of 2 and 3?";
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: "user",
content: prompt,
},
];
console.log("Querying LLM: ", messages[0].content);
let response = this.openai.chat.completions.create({
model: "gpt-4o-mini",
max_tokens: 1000,
messages,
tools: tools,
});
let results: any[] = [];
// 1. Go through the LLM response,for each choice, check if it has tool calls
(await response).choices.map(async (choice: { message: any; }) => {
const message = choice.message;
if (message.tool_calls) {
console.log("Making tool call")
await this.callTools(message.tool_calls, results);
}
});
}
}
let client = new MyClient();
const transport = new StdioClientTransport({
command: "node",
args: ["./build/index.js"]
});
client.connectToServer(transport);-
Agreguemos algunas importaciones necesarias para llamar a un LLM:
# llm import os from azure.ai.inference import ChatCompletionsClient from azure.ai.inference.models import SystemMessage, UserMessage from azure.core.credentials import AzureKeyCredential import json
-
Luego, agreguemos la función que llamará al LLM:
# llm def call_llm(prompt, functions): token = os.environ["GITHUB_TOKEN"] endpoint = "https://models.inference.ai.azure.com" model_name = "gpt-4o" client = ChatCompletionsClient( endpoint=endpoint, credential=AzureKeyCredential(token), ) print("CALLING LLM") response = client.complete( messages=[ { "role": "system", "content": "You are a helpful assistant.", }, { "role": "user", "content": prompt, }, ], model=model_name, tools = functions, # Optional parameters temperature=1., max_tokens=1000, top_p=1. ) response_message = response.choices[0].message functions_to_call = [] if response_message.tool_calls: for tool_call in response_message.tool_calls: print("TOOL: ", tool_call) name = tool_call.function.name args = json.loads(tool_call.function.arguments) functions_to_call.append({ "name": name, "args": args }) return functions_to_call
En el código anterior hemos:
- Pasado nuestras funciones, que encontramos en el servidor MCP y convertimos, al LLM.
- Luego llamamos al LLM con dichas funciones.
- Luego, inspeccionamos el resultado para ver qué funciones debemos llamar, si es que hay alguna.
- Finalmente, pasamos un array de funciones para llamar.
-
Último paso, actualicemos nuestro código principal:
prompt = "Add 2 to 20" # ask LLM what tools to all, if any functions_to_call = call_llm(prompt, functions) # call suggested functions for f in functions_to_call: result = await session.call_tool(f["name"], arguments=f["args"]) print("TOOLS result: ", result.content)
Ahí, ese fue el último paso, en el código anterior estamos:
- Llamando a una herramienta MCP a través de
call_toolusando una función que el LLM pensó que deberíamos llamar según nuestro prompt. - Imprimiendo el resultado de la llamada a la herramienta en el servidor MCP.
- Llamando a una herramienta MCP a través de
-
Mostremos algo de código para hacer una solicitud de prompt al LLM:
var tools = await GetMcpTools(); for (int i = 0; i < tools.Count; i++) { var tool = tools[i]; Console.WriteLine($"MCP Tools def: {i}: {tool}"); } // 0. Define the chat history and the user message var userMessage = "add 2 and 4"; chatHistory.Add(new ChatRequestUserMessage(userMessage)); // 1. Define tools ChatCompletionsToolDefinition def = CreateToolDefinition(); // 2. Define options, including the tools var options = new ChatCompletionsOptions(chatHistory) { Model = "gpt-4o-mini", Tools = { tools[0] } }; // 3. Call the model ChatCompletions? response = await client.CompleteAsync(options); var content = response.Content;
En el código anterior hemos:
- Recuperado herramientas del servidor MCP,
var tools = await GetMcpTools(). - Definido un prompt del usuario
userMessage. - Construido un objeto de opciones especificando el modelo y las herramientas.
- Realizado una solicitud hacia el LLM.
- Recuperado herramientas del servidor MCP,
-
Un último paso, veamos si el LLM piensa que debemos llamar a una función:
// 4. Check if the response contains a function call ChatCompletionsToolCall? calls = response.ToolCalls.FirstOrDefault(); for (int i = 0; i < response.ToolCalls.Count; i++) { var call = response.ToolCalls[i]; Console.WriteLine($"Tool call {i}: {call.Name} with arguments {call.Arguments}"); //Tool call 0: add with arguments {"a":2,"b":4} var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(call.Arguments); var result = await mcpClient.CallToolAsync( call.Name, dict!, cancellationToken: CancellationToken.None ); Console.WriteLine(result.Content.First(c => c.Type == "text").Text); }
En el código anterior hemos:
- Recorrido una lista de llamadas a funciones.
- Para cada llamada a herramienta, extraemos el nombre y los argumentos y llamamos a la herramienta en el servidor MCP usando el cliente MCP. Finalmente imprimimos los resultados.
Aquí está el código completo:
using Azure;
using Azure.AI.Inference;
using Azure.Identity;
using System.Text.Json;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
using System.Text.Json;
var endpoint = "https://models.inference.ai.azure.com";
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); // Your GitHub Access Token
var client = new ChatCompletionsClient(new Uri(endpoint), new AzureKeyCredential(token));
var chatHistory = new List<ChatRequestMessage>
{
new ChatRequestSystemMessage("You are a helpful assistant that knows about AI")
};
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "/workspaces/mcp-for-beginners/03-GettingStarted/02-client/solution/server/bin/Debug/net8.0/server",
Arguments = [],
});
Console.WriteLine("Setting up stdio transport");
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport);
ChatCompletionsToolDefinition ConvertFrom(string name, string description, JsonElement jsonElement)
{
// convert the tool to a function definition
FunctionDefinition functionDefinition = new FunctionDefinition(name)
{
Description = description,
Parameters = BinaryData.FromObjectAsJson(new
{
Type = "object",
Properties = jsonElement
},
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
};
// create a tool definition
ChatCompletionsToolDefinition toolDefinition = new ChatCompletionsToolDefinition(functionDefinition);
return toolDefinition;
}
async Task<List<ChatCompletionsToolDefinition>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatCompletionsToolDefinition> toolDefinitions = new List<ChatCompletionsToolDefinition>();
foreach (var tool in tools)
{
Console.WriteLine($"Connected to server with tools: {tool.Name}");
Console.WriteLine($"Tool description: {tool.Description}");
Console.WriteLine($"Tool parameters: {tool.JsonSchema}");
JsonElement propertiesElement;
tool.JsonSchema.TryGetProperty("properties", out propertiesElement);
var def = ConvertFrom(tool.Name, tool.Description, propertiesElement);
Console.WriteLine($"Tool definition: {def}");
toolDefinitions.Add(def);
Console.WriteLine($"Properties: {propertiesElement}");
}
return toolDefinitions;
}
// 1. List tools on mcp server
var tools = await GetMcpTools();
for (int i = 0; i < tools.Count; i++)
{
var tool = tools[i];
Console.WriteLine($"MCP Tools def: {i}: {tool}");
}
// 2. Define the chat history and the user message
var userMessage = "add 2 and 4";
chatHistory.Add(new ChatRequestUserMessage(userMessage));
// 3. Define options, including the tools
var options = new ChatCompletionsOptions(chatHistory)
{
Model = "gpt-4o-mini",
Tools = { tools[0] }
};
// 4. Call the model
ChatCompletions? response = await client.CompleteAsync(options);
var content = response.Content;
// 5. Check if the response contains a function call
ChatCompletionsToolCall? calls = response.ToolCalls.FirstOrDefault();
for (int i = 0; i < response.ToolCalls.Count; i++)
{
var call = response.ToolCalls[i];
Console.WriteLine($"Tool call {i}: {call.Name} with arguments {call.Arguments}");
//Tool call 0: add with arguments {"a":2,"b":4}
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(call.Arguments);
var result = await mcpClient.CallToolAsync(
call.Name,
dict!,
cancellationToken: CancellationToken.None
);
Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
}
// 5. Print the generic response
Console.WriteLine($"Assistant response: {content}");try {
// Execute natural language requests that automatically use MCP tools
String response = bot.chat("Calculate the sum of 24.5 and 17.3 using the calculator service");
System.out.println(response);
response = bot.chat("What's the square root of 144?");
System.out.println(response);
response = bot.chat("Show me the help for the calculator service");
System.out.println(response);
} finally {
mcpClient.close();
}En el código anterior hemos:
- Usado prompts simples en lenguaje natural para interactuar con las herramientas del servidor MCP.
- El marco LangChain4j maneja automáticamente:
- Convertir prompts del usuario en llamadas a herramientas cuando sea necesario.
- Llamar a las herramientas MCP apropiadas según la decisión del LLM.
- Gestionar el flujo de conversación entre el LLM y el servidor MCP.
- El método
bot.chat()devuelve respuestas en lenguaje natural que pueden incluir resultados de ejecuciones de herramientas MCP. - Este enfoque proporciona una experiencia de usuario fluida donde los usuarios no necesitan conocer la implementación subyacente de MCP.
Ejemplo completo de código:
public class LangChain4jClient {
public static void main(String[] args) throws Exception { ChatLanguageModel model = OpenAiOfficialChatModel.builder()
.isGitHubModels(true)
.apiKey(System.getenv("GITHUB_TOKEN"))
.timeout(Duration.ofSeconds(60))
.modelName("gpt-4.1-nano")
.timeout(Duration.ofSeconds(60))
.build();
McpTransport transport = new HttpMcpTransport.Builder()
.sseUrl("http://localhost:8080/sse")
.timeout(Duration.ofSeconds(60))
.logRequests(true)
.logResponses(true)
.build();
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
Bot bot = AiServices.builder(Bot.class)
.chatLanguageModel(model)
.toolProvider(toolProvider)
.build();
try {
String response = bot.chat("Calculate the sum of 24.5 and 17.3 using the calculator service");
System.out.println(response);
response = bot.chat("What's the square root of 144?");
System.out.println(response);
response = bot.chat("Show me the help for the calculator service");
System.out.println(response);
} finally {
mcpClient.close();
}
}
}Aquí es donde ocurre la mayor parte del trabajo. Llamaremos al LLM con el prompt inicial del usuario, luego procesaremos la respuesta para ver si se necesitan llamar herramientas. Si es así, llamaremos a esas herramientas y continuaremos la conversación con el LLM hasta que no se necesiten más llamadas a herramientas y tengamos una respuesta final.
Agreguemos una función que manejará las llamadas al LLM. Añade la siguiente función a tu archivo main.rs:
async fn call_llm(
client: &Client<OpenAIConfig>,
messages: &[Value],
tools: &ListToolsResult,
) -> Result<Value, Box<dyn Error>> {
let response = client
.completions()
.create_byot(json!({
"messages": messages,
"model": "openai/gpt-4.1",
"tools": format_tools(tools).await?,
}))
.await?;
Ok(response)
}Esta función toma el cliente LLM, una lista de mensajes (incluyendo el mensaje del usuario), herramientas del servidor MCP, y envía una solicitud al LLM, devolviendo la respuesta.
La respuesta del LLM contendrá un arreglo de choices. Necesitaremos procesar el resultado para verificar si hay tool_calls presentes. Esto nos indica que el LLM está solicitando que se llame a una herramienta específica con argumentos. Añade el siguiente código al final de tu archivo main.rs para definir una función que maneje la respuesta del LLM:
async fn process_llm_response(
llm_response: &Value,
mcp_client: &RunningService<RoleClient, ()>,
openai_client: &Client<OpenAIConfig>,
mcp_tools: &ListToolsResult,
messages: &mut Vec<Value>,
) -> Result<(), Box<dyn Error>> {
let Some(message) = llm_response
.get("choices")
.and_then(|c| c.as_array())
.and_then(|choices| choices.first())
.and_then(|choice| choice.get("message"))
else {
return Ok(());
};
// Print content if available
if let Some(content) = message.get("content").and_then(|c| c.as_str()) {
println!("🤖 {}", content);
}
// Handle tool calls
if let Some(tool_calls) = message.get("tool_calls").and_then(|tc| tc.as_array()) {
messages.push(message.clone()); // Add assistant message
// Execute each tool call
for tool_call in tool_calls {
let (tool_id, name, args) = extract_tool_call_info(tool_call)?;
println!("⚡ Calling tool: {}", name);
let result = mcp_client
.call_tool(CallToolRequestParam {
name: name.into(),
arguments: serde_json::from_str::<Value>(&args)?.as_object().cloned(),
})
.await?;
// Add tool result to messages
messages.push(json!({
"role": "tool",
"tool_call_id": tool_id,
"content": serde_json::to_string_pretty(&result)?
}));
}
// Continue conversation with tool results
let response = call_llm(openai_client, messages, mcp_tools).await?;
Box::pin(process_llm_response(
&response,
mcp_client,
openai_client,
mcp_tools,
messages,
))
.await?;
}
Ok(())
}Si hay tool_calls presentes, extrae la información de la herramienta, llama al servidor MCP con la solicitud de la herramienta y agrega los resultados a los mensajes de la conversación. Luego, continúa la conversación con el LLM y los mensajes se actualizan con la respuesta del asistente y los resultados de la llamada a la herramienta.
Para extraer la información de la llamada a la herramienta que el LLM devuelve para las llamadas MCP, añadiremos otra función auxiliar para extraer todo lo necesario para realizar la llamada. Añade el siguiente código al final de tu archivo main.rs:
fn extract_tool_call_info(tool_call: &Value) -> Result<(String, String, String), Box<dyn Error>> {
let tool_id = tool_call
.get("id")
.and_then(|id| id.as_str())
.unwrap_or("")
.to_string();
let function = tool_call.get("function").ok_or("Missing function")?;
let name = function
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string();
let args = function
.get("arguments")
.and_then(|a| a.as_str())
.unwrap_or("{}")
.to_string();
Ok((tool_id, name, args))
}Con todas las piezas en su lugar, ahora podemos manejar el mensaje inicial del usuario y llamar al LLM. Actualiza tu función main para incluir el siguiente código:
// LLM conversation with tool calls
let response = call_llm(&openai_client, &messages, &tools).await?;
process_llm_response(
&response,
&mcp_client,
&openai_client,
&tools,
&mut messages,
)
.await?;Esto consultará al LLM con el mensaje inicial del usuario pidiendo la suma de dos números, y procesará la respuesta para manejar dinámicamente las llamadas a herramientas.
¡Genial, lo lograste!
Toma el código del ejercicio y amplía el servidor con más herramientas. Luego, crea un cliente con un LLM, como en el ejercicio, y pruébalo con diferentes mensajes para asegurarte de que todas las herramientas de tu servidor se llamen dinámicamente. Este enfoque para construir un cliente significa que el usuario final tendrá una gran experiencia, ya que podrá usar mensajes en lugar de comandos exactos del cliente, sin darse cuenta de que se está llamando a un servidor MCP.
- Agregar un LLM a tu cliente proporciona una mejor forma para que los usuarios interactúen con los servidores MCP.
- Necesitas convertir la respuesta del servidor MCP en algo que el LLM pueda entender.
- Calculadora en Java
- Calculadora en .Net
- Calculadora en JavaScript
- Calculadora en TypeScript
- Calculadora en Python
- Calculadora en Rust
Descargo de responsabilidad:
Este documento ha sido traducido utilizando el servicio de traducción automática Co-op Translator. Aunque nos esforzamos por garantizar la precisión, tenga en cuenta que las traducciones automatizadas pueden contener errores o imprecisiones. El documento original en su idioma nativo debe considerarse como la fuente autorizada. Para información crítica, se recomienda una traducción profesional realizada por humanos. No nos hacemos responsables de malentendidos o interpretaciones erróneas que puedan surgir del uso de esta traducción.