Tới giờ bạn đã biết tạo server và client. Client gọi tường minh tới server để liệt kê tool, resource và prompt. Nhưng cách này không thực tế lắm. Người dùng sống trong thời đại agent và muốn dùng prompt, nói chuyện với một LLM. Họ không quan tâm bạn có dùng MCP để chứa các khả năng (capability) hay không; họ chỉ muốn tương tác bằng ngôn ngữ tự nhiên. Vậy giải quyết thế nào? Lời giải là thêm một LLM vào client.
Tổng quan
Bài này tập trung vào việc thêm LLM vào client và cho thấy điều đó mang lại trải nghiệm tốt hơn nhiều cho người dùng.
Mục tiêu học tập
Sau bài này, bạn sẽ có thể:
- Tạo client có LLM.
- Tương tác liền mạch với MCP server qua LLM.
- Mang lại trải nghiệm tốt hơn cho người dùng cuối ở phía client.
Cách tiếp cận
Hãy hiểu cách tiếp cận cần dùng. Thêm LLM nghe đơn giản, nhưng thực tế ta làm thế nào?
Đây là cách client tương tác với server:
- Thiết lập kết nối với server.
- Liệt kê capability, prompt, resource và tool, lưu lại schema của chúng.
- Thêm LLM và truyền các capability đã lưu cùng schema theo định dạng LLM hiểu.
- Xử lý prompt của người dùng bằng cách gửi nó tới LLM cùng các tool client đã liệt kê.
Tốt, giờ đã hiểu tổng quan, hãy thử trong bài thực hành dưới đây.
Bài thực hành: tạo client có LLM
Cấu hình Microsoft Foundry
GitHub Models đã ngừng hoạt động từ ngày 30/7/2026. Hãy tạo một resource Microsoft Foundry, triển khai (deploy) một model đang hoạt động như gpt-5.1, rồi đặt các biến môi trường:
export AZURE_OPENAI_ENDPOINT="https://<resource-name>.openai.azure.com"
export AZURE_OPENAI_API_KEY="<api-key>"
export AZURE_OPENAI_DEPLOYMENT="gpt-5.1"
Dùng tên deployment trong lời gọi API; nó có thể khác tên model bên dưới. Hãy xem lịch ngừng hỗ trợ model của Microsoft Foundry trước khi chọn model.
Bước 1: kết nối tới server
Trước hết hãy tạo client:
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(){
const endpoint = process.env.AZURE_OPENAI_ENDPOINT;
const apiKey = process.env.AZURE_OPENAI_API_KEY;
if (!endpoint || !apiKey) {
throw new Error("AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY must be set");
}
this.openai = new OpenAI({
baseURL: `${endpoint.replace(/\/$/, "")}/openai/v1/`,
apiKey,
});
this.client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {}
}
);
}
}
Trong code trên, chúng ta:
- Import các thư viện cần thiết.
- Tạo một lớp có hai thành viên,
clientvàopenai, lần lượt để quản lý client và tương tác với LLM. - Cấu hình client OpenAI dùng endpoint v1 và API key của Microsoft Foundry.
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())
Trong code trên, chúng ta:
- Import các thư viện MCP cần thiết.
- Tạo một client.
using ModelContextProtocol.Client;
using OpenAI;
using OpenAI.Chat;
using System.ClientModel;
using System.Text.Json;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-5.1";
var client = new ChatClient(
deployment,
new ApiKeyCredential(apiKey),
new OpenAIClientOptions
{
Endpoint = new Uri($"{endpoint.TrimEnd('/')}/openai/v1/")
});
var clientTransport = new StdioClientTransport(new()
{
Name = "Demo Server",
Command = "/workspaces/mcp-for-beginners/03-GettingStarted/02-client/solution/server/bin/Debug/net9.0/server",
Arguments = [],
});
await using var mcpClient = await McpClient.CreateAsync(clientTransport);
Trước hết, thêm các dependency LangChain4j vào pom.xml để bật tích hợp MCP và API MiniMax tương thích OpenAI:
<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>
<!-- Spring Boot Starter (optional, for production apps) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
Đặt API key MiniMax và (tuỳ chọn) endpoint, model. MINIMAX_MODEL_ID hỗ trợ MiniMax-M3 và MiniMax-M2.7. Nếu không đặt OPENAI_BASE_URL, MINIMAX_REGION hỗ trợ global_en và cn_zh.
export OPENAI_API_KEY=your_minimax_api_key_here
export OPENAI_BASE_URL=https://api.minimax.io/v1
export MINIMAX_MODEL_ID=MiniMax-M3
Để chọn endpoint theo vùng, bỏ OPENAI_BASE_URL:
unset OPENAI_BASE_URL
export MINIMAX_REGION=cn_zh
Sau đó tạo lớp client 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;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
public class LangChain4jClient {
private static final String DEFAULT_BASE_URL = "https://api.minimax.io/v1";
private static final String DEFAULT_MODEL_ID = "MiniMax-M3";
private static final Map<String, String> REGIONAL_BASE_URLS = Map.of(
"global_en", "https://api.minimax.io/v1",
"cn_zh", "https://api.minimaxi.com/v1");
private static final Set<String> SUPPORTED_MODEL_IDS = Set.of("MiniMax-M3", "MiniMax-M2.7");
public static void main(String[] args) throws Exception {
ChatLanguageModel model = OpenAiOfficialChatModel.builder()
.baseUrl(resolveBaseUrl())
.apiKey(requireEnv("OPENAI_API_KEY"))
.timeout(Duration.ofSeconds(60))
.modelName(resolveModelName())
.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();
}
private static String resolveBaseUrl() {
String baseUrl = System.getenv("OPENAI_BASE_URL");
if (baseUrl != null && !baseUrl.isBlank()) {
return baseUrl;
}
String region = System.getenv("MINIMAX_REGION");
if (region == null || region.isBlank()) {
return DEFAULT_BASE_URL;
}
String regionalBaseUrl = REGIONAL_BASE_URLS.get(region);
if (regionalBaseUrl == null) {
throw new IllegalArgumentException("Unsupported MINIMAX_REGION value: " + region
+ ". Supported values: " + new TreeSet<>(REGIONAL_BASE_URLS.keySet()));
}
return regionalBaseUrl;
}
private static String resolveModelName() {
String modelId = System.getenv("MINIMAX_MODEL_ID");
if (modelId == null || modelId.isBlank()) {
return DEFAULT_MODEL_ID;
}
if (!SUPPORTED_MODEL_IDS.contains(modelId)) {
throw new IllegalArgumentException("Unsupported MINIMAX_MODEL_ID value: " + modelId
+ ". Supported values: " + new TreeSet<>(SUPPORTED_MODEL_IDS));
}
return modelId;
}
private static String requireEnv(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
throw new IllegalStateException(name + " environment variable is not set");
}
return value;
}
}
Trong code trên, chúng ta:
- Thêm dependency LangChain4j: cần cho tích hợp MCP và API MiniMax tương thích OpenAI.
- Import thư viện LangChain4j: cho tích hợp MCP và chức năng chat model OpenAI.
- Tạo một
ChatLanguageModel: cấu hình dùng MiniMax với API key, endpoint và model ID được hỗ trợ. - Thiết lập transport HTTP: dùng Server-Sent Events (SSE) để kết nối tới MCP server.
- Tạo MCP client: xử lý giao tiếp với server.
- Dùng hỗ trợ MCP có sẵn của LangChain4j: giúp tích hợp LLM với MCP server đơn giản hơn.
Ví dụ này giả định bạn có một MCP server viết bằng Rust đang chạy. Nếu chưa có, xem lại bài MCP server đầu tiên để tạo.
Khi đã có server Rust, mở terminal và vào cùng thư mục với server. Chạy lệnh sau để tạo project client LLM mới:
mkdir calculator-llmclient
cd calculator-llmclient
cargo init
Thêm các dependency sau vào 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"] }
Không có thư viện Rust chính thức cho OpenAI, nhưng crate async-openai là thư viện do cộng đồng duy trì được dùng phổ biến.
Mở src/main.rs và thay nội dung bằng code sau:
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 Microsoft Foundry client
let endpoint = std::env::var("AZURE_OPENAI_ENDPOINT")?;
let api_key = std::env::var("AZURE_OPENAI_API_KEY")?;
let openai_client = Client::with_config(
OpenAIConfig::new()
.with_api_base(format!("{}/openai/v1", endpoint.trim_end_matches('/')))
.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(())
}
Code này dựng một ứng dụng Rust kết nối tới MCP server và một deployment model trên Microsoft Foundry để tương tác với LLM.
Quan trọng: đặt AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY và AZURE_OPENAI_DEPLOYMENT trước khi chạy ứng dụng.
Tốt, bước tiếp theo là liệt kê capability của server.
Bước 2: liệt kê capability của server
Giờ ta kết nối tới server và hỏi các capability của nó:
Trong cùng lớp, thêm các phương thức sau:
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();
}
Trong code trên, chúng ta:
- Thêm code kết nối tới server,
connectToServer. - Tạo phương thức
runđiều phối luồng ứng dụng. Tới giờ nó chỉ liệt kê tool, sẽ thêm phần khác ngay sau đây.
# 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"])
Những gì ta thêm: liệt kê resource và tool rồi in ra. Với tool, ta còn in inputSchema để dùng sau.
async Task<List<ChatTool>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatTool> toolDefinitions = [];
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;
}
Trong code trên, chúng ta:
- Liệt kê các tool có trên MCP server.
- Với mỗi tool, in tên, mô tả và schema. Schema sẽ được dùng để gọi tool ngay sau đây.
// 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 responses
Trong code trên, chúng ta:
- Tạo một
McpToolProvidertự động khám phá và đăng ký mọi tool từ MCP server. - Tool provider tự xử lý bên trong việc chuyển schema tool MCP sang định dạng tool của LangChain4j.
- Cách này che đi quá trình liệt kê và chuyển đổi tool thủ công.
Lấy tool từ MCP server bằng phương thức list_tools. Trong hàm main, sau khi thiết lập MCP client, thêm code:
// Get MCP tool listing
let tools = mcp_client.list_tools(Default::default()).await?;
Bước 3: chuyển capability của server thành tool cho LLM
Sau khi liệt kê capability của server, bước tiếp theo là chuyển chúng sang định dạng LLM hiểu. Khi đó, ta có thể cung cấp các capability này làm tool cho LLM.
-
Thêm code sau để chuyển phản hồi từ MCP server sang định dạng tool LLM dùng được:
typescriptopenAiToolAdapter(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, }, }, }; }Code trên nhận phản hồi từ MCP server và chuyển thành định nghĩa tool mà LLM hiểu.
-
Cập nhật phương thức
runđể liệt kê capability của server:typescriptasync 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, }); }); }Ở đây,
runduyệt qua kết quả và gọiopenAiToolAdaptercho từng phần tử.
-
Trước hết, tạo hàm chuyển đổi sau:
pythondef 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_schemaHàm này nhận phản hồi tool của MCP và chuyển sang định dạng LLM hiểu.
-
Tiếp theo, cập nhật code client để dùng hàm này:
pythonfunctions = [] for tool in tools.tools: print("Tool: ", tool.name) print("Tool", tool.inputSchema["properties"]) functions.append(convert_to_llm_tool(tool))Ở đây ta gọi
convert_to_llm_toolđể chuyển phản hồi tool MCP thành thứ đưa được cho LLM sau này.
-
Thêm code chuyển phản hồi tool MCP sang thứ LLM hiểu:
csharpChatTool ConvertFrom(string name, string description, JsonElement jsonElement) { return ChatTool.CreateFunctionTool( functionName: name, functionDescription: description, functionParameters: BinaryData.FromString(jsonElement.GetRawText())); }Trong code trên, ta tạo hàm
ConvertFromnhận tên, mô tả và input schema, rồi tạo mộtChatToolcủa OpenAI từ JSON schema của tool MCP. -
Cập nhật code có sẵn để dùng hàm trên:
csharpasync Task<List<ChatTool>> GetMcpTools() { Console.WriteLine("Listing tools"); var tools = await mcpClient.ListToolsAsync(); List<ChatTool> toolDefinitions = []; 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}"); var def = ConvertFrom(tool.Name, tool.Description, tool.JsonSchema); Console.WriteLine($"Tool definition: {def}"); toolDefinitions.Add(def); } return toolDefinitions; }
Trong code trên, ta cập nhật hàm để chuyển phản hồi tool MCP thành tool cho LLM. Phần code được thêm:
JsonElement propertiesElement;
tool.JsonSchema.TryGetProperty("properties", out propertiesElement);
var def = ConvertFrom(tool.Name, tool.Description, propertiesElement);
Console.WriteLine($"Tool definition: {def}");
toolDefinitions.Add(def);
Input schema là một phần của phản hồi tool nhưng nằm trong thuộc tính "properties", nên cần trích ra. Sau đó ta gọi ConvertFrom với chi tiết của tool. Phần việc nặng nhất đã xong, giờ hãy xem mọi thứ ghép lại ra sao khi xử lý prompt của người dùng.
// 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();
Trong code trên, chúng ta:
- Định nghĩa interface
Botđơn giản cho tương tác bằng ngôn ngữ tự nhiên. - Dùng
AiServicescủa LangChain4j để tự động gắn LLM với MCP tool provider. - Framework tự xử lý chuyển đổi schema tool và function calling phía sau.
- Cách này bỏ hẳn việc chuyển đổi tool thủ công; LangChain4j lo toàn bộ phần phức tạp của việc chuyển tool MCP sang định dạng LLM dùng được.
Để chuyển phản hồi tool MCP sang định dạng LLM hiểu, ta thêm một hàm hỗ trợ định dạng danh sách tool. Thêm code sau vào main.rs, bên dưới hàm main. Hàm này được gọi khi gửi yêu cầu tới 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)
}
Tốt, tiếp theo là xử lý yêu cầu của người dùng.
Bước 4: xử lý prompt của người dùng
Ở phần này, ta xử lý yêu cầu (request) của người dùng.
-
Thêm một phương thức dùng để gọi tool theo phản hồi của LLM:
typescriptasync 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 } }Trong code trên, chúng ta thêm phương thức
callTools. Phương thức nhận phản hồi của LLM, kiểm tra xem tool nào được gọi (nếu có), và gọi tool trên server nếu LLM cho biết cần gọi. -
Cập nhật phương thức
runđể gọi LLM và gọicallTools:typescript// 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: process.env.AZURE_OPENAI_DEPLOYMENT ?? "gpt-5.1", max_completion_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); } });
Đây là code đầy đủ:
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(){
const endpoint = process.env.AZURE_OPENAI_ENDPOINT;
const apiKey = process.env.AZURE_OPENAI_API_KEY;
if (!endpoint || !apiKey) {
throw new Error("AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY must be set");
}
this.openai = new OpenAI({
baseURL: `${endpoint.replace(/\/$/, "")}/openai/v1/`,
apiKey,
});
this.client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {}
}
);
}
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: process.env.AZURE_OPENAI_DEPLOYMENT ?? "gpt-5.1",
max_completion_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);
}
});
}
}
let client = new MyClient();
const transport = new StdioClientTransport({
command: "node",
args: ["./build/index.js"]
});
client.connectToServer(transport);
-
Thêm các import cần để gọi LLM:
python# llm import os from openai import OpenAI import json -
Tiếp theo, thêm hàm gọi LLM. Ví dụ này dùng model đang hoạt động
gpt-5.1. Hãy xem lịch ngừng hỗ trợ model của Microsoft Foundry khi chọn model cho ứng dụng.python# llm def call_llm(prompt, functions): client = OpenAI( base_url=( f"{os.environ['AZURE_OPENAI_ENDPOINT'].rstrip('/')}/openai/v1/" ), api_key=os.environ["AZURE_OPENAI_API_KEY"], ) print("CALLING LLM") response = client.chat.completions.create( messages=[ { "role": "system", "content": "You are a helpful assistant.", }, { "role": "user", "content": prompt, }, ], model=os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-5.1"), tools=functions, max_completion_tokens=1000, ) 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_callTrong code trên, chúng ta:
- Truyền cho LLM các function đã tìm thấy trên MCP server và đã chuyển đổi.
- Gọi LLM với các function đó.
- Xem kết quả để biết cần gọi function nào, nếu có.
- Cuối cùng trả về mảng các function cần gọi.
-
Bước cuối, cập nhật code chính:
pythonprompt = "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)Trong code trên, chúng ta gọi một tool MCP qua
call_toolbằng function mà LLM cho là cần gọi dựa trên prompt, rồi in kết quả lời gọi tool tới MCP server.
-
Code gửi yêu cầu prompt tới LLM:
csharpvar 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 UserChatMessage(userMessage)); // 2. Define options, including the tools var options = new ChatCompletionOptions { Tools = { tools[0] } }; // 3. Call the model ChatCompletion response = await client.CompleteChatAsync(chatHistory, options); var content = response.Content.FirstOrDefault()?.Text;Trong code trên, chúng ta lấy tool từ MCP server (
var tools = await GetMcpTools()), định nghĩa prompt người dùnguserMessage, dựng đối tượng options chỉ định model và tool, rồi gửi yêu cầu tới LLM. -
Bước cuối, xem LLM có cho rằng cần gọi function không:
csharp// 4. Check if the response contains a function call for (int i = 0; i < response.ToolCalls.Count; i++) { var call = response.ToolCalls[i]; Console.WriteLine($"Tool call {i}: {call.FunctionName} with arguments {call.FunctionArguments}"); //Tool call 0: add with arguments {"a":2,"b":4} var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(call.FunctionArguments); var result = await mcpClient.CallToolAsync( call.FunctionName, dict!, cancellationToken: CancellationToken.None ); var textBlock = result.Content.OfType<TextContentBlock>().FirstOrDefault(); if (textBlock != null) { Console.WriteLine(textBlock.Text); } }Trong code trên, ta duyệt danh sách function call; với mỗi tool call, tách tên và đối số rồi gọi tool trên MCP server bằng MCP client, cuối cùng in kết quả.
Đây là code đầy đủ:
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using OpenAI;
using OpenAI.Chat;
using System.ClientModel;
using System.Text.Json;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-5.1";
if (string.IsNullOrWhiteSpace(endpoint) || string.IsNullOrWhiteSpace(apiKey))
{
Console.WriteLine("Please set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY.");
return;
}
var client = new ChatClient(
model: deployment,
credential: new ApiKeyCredential(apiKey),
options: new OpenAIClientOptions
{
Endpoint = new Uri($"{endpoint.TrimEnd('/')}/openai/v1/")
});
var chatHistory = new List<ChatMessage>
{
new SystemChatMessage("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/net9.0/server",
Arguments = [],
});
Console.WriteLine("Setting up stdio transport");
await using var mcpClient = await McpClient.CreateAsync(clientTransport);
ChatTool ConvertFrom(string name, string description, JsonElement jsonElement)
{
return ChatTool.CreateFunctionTool(
functionName: name,
functionDescription: description,
functionParameters: BinaryData.FromString(jsonElement.GetRawText()));
}
async Task<List<ChatTool>> GetMcpTools()
{
Console.WriteLine("Listing tools");
var tools = await mcpClient.ListToolsAsync();
List<ChatTool> toolDefinitions = [];
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}");
var def = ConvertFrom(tool.Name, tool.Description, tool.JsonSchema);
Console.WriteLine($"Tool definition: {def}");
toolDefinitions.Add(def);
}
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 UserChatMessage(userMessage));
// 3. Define options, including the tools
var options = new ChatCompletionOptions
{
Tools = { tools[0] }
};
// 4. Call the model
ChatCompletion response = await client.CompleteChatAsync(chatHistory, options);
var content = response.Content.FirstOrDefault()?.Text;
// 5. Check if the response contains a function call
for (int i = 0; i < response.ToolCalls.Count; i++)
{
var call = response.ToolCalls[i];
Console.WriteLine($"Tool call {i}: {call.FunctionName} with arguments {call.FunctionArguments}");
//Tool call 0: add with arguments {"a":2,"b":4}
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(call.FunctionArguments);
var result = await mcpClient.CallToolAsync(
call.FunctionName,
dict!,
cancellationToken: CancellationToken.None
);
var textBlock = result.Content.OfType<TextContentBlock>().FirstOrDefault();
if (textBlock != null)
{
Console.WriteLine(textBlock.Text);
}
}
// 6. 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();
}
Trong code trên, chúng ta:
- Dùng prompt ngôn ngữ tự nhiên đơn giản để tương tác với tool của MCP server.
- Framework LangChain4j tự động: chuyển prompt người dùng thành tool call khi cần; gọi đúng tool MCP theo quyết định của LLM; quản lý luồng hội thoại giữa LLM và MCP server.
- Phương thức
bot.chat()trả về câu trả lời bằng ngôn ngữ tự nhiên, có thể chứa kết quả chạy tool MCP. - Cách này mang lại trải nghiệm liền mạch: người dùng không cần biết gì về phần cài đặt MCP bên dưới.
Code đầy đủ:
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;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
public class LangChain4jClient {
private static final String DEFAULT_BASE_URL = "https://api.minimax.io/v1";
private static final String DEFAULT_MODEL_ID = "MiniMax-M3";
private static final Map<String, String> REGIONAL_BASE_URLS = Map.of(
"global_en", "https://api.minimax.io/v1",
"cn_zh", "https://api.minimaxi.com/v1");
private static final Set<String> SUPPORTED_MODEL_IDS = Set.of("MiniMax-M3", "MiniMax-M2.7");
public static void main(String[] args) throws Exception {
ChatLanguageModel model = OpenAiOfficialChatModel.builder()
.baseUrl(resolveBaseUrl())
.apiKey(requireEnv("OPENAI_API_KEY"))
.timeout(Duration.ofSeconds(60))
.modelName(resolveModelName())
.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();
}
}
private static String resolveBaseUrl() {
String baseUrl = System.getenv("OPENAI_BASE_URL");
if (baseUrl != null && !baseUrl.isBlank()) {
return baseUrl;
}
String region = System.getenv("MINIMAX_REGION");
if (region == null || region.isBlank()) {
return DEFAULT_BASE_URL;
}
String regionalBaseUrl = REGIONAL_BASE_URLS.get(region);
if (regionalBaseUrl == null) {
throw new IllegalArgumentException("Unsupported MINIMAX_REGION value: " + region
+ ". Supported values: " + new TreeSet<>(REGIONAL_BASE_URLS.keySet()));
}
return regionalBaseUrl;
}
private static String resolveModelName() {
String modelId = System.getenv("MINIMAX_MODEL_ID");
if (modelId == null || modelId.isBlank()) {
return DEFAULT_MODEL_ID;
}
if (!SUPPORTED_MODEL_IDS.contains(modelId)) {
throw new IllegalArgumentException("Unsupported MINIMAX_MODEL_ID value: " + modelId
+ ". Supported values: " + new TreeSet<>(SUPPORTED_MODEL_IDS));
}
return modelId;
}
private static String requireEnv(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
throw new IllegalStateException(name + " environment variable is not set");
}
return value;
}
}
Đây là nơi diễn ra phần lớn công việc. Ta gọi LLM với prompt ban đầu của người dùng, rồi xử lý phản hồi để xem có tool nào cần gọi không. Nếu có, ta gọi các tool đó và tiếp tục hội thoại với LLM cho tới khi không còn tool call nào và có câu trả lời cuối.
Vì sẽ gọi LLM nhiều lần, hãy định nghĩa một hàm xử lý lời gọi LLM. Thêm hàm sau vào main.rs:
async fn call_llm(
client: &Client<OpenAIConfig>,
messages: &[Value],
tools: &ListToolsResult,
) -> Result<Value, Box<dyn Error>> {
let model = std::env::var("AZURE_OPENAI_DEPLOYMENT")
.unwrap_or_else(|_| "gpt-5.1".to_string());
let response = client
.completions()
.create_byot(json!({
"messages": messages,
"model": model,
"tools": format_tools(tools).await?,
}))
.await?;
Ok(response)
}
Hàm này nhận client LLM, danh sách message (gồm prompt người dùng), các tool từ MCP server, gửi yêu cầu tới LLM và trả về phản hồi.
Phản hồi của LLM chứa mảng choices. Ta cần xem có tool_calls nào không; nếu có, nghĩa là LLM yêu cầu gọi một tool cụ thể với các đối số. Thêm code sau vào cuối main.rs để định nghĩa hàm xử lý phản hồi 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(())
}
Nếu có tool_calls, hàm trích thông tin tool, gọi MCP server với yêu cầu đó và thêm kết quả vào message của hội thoại. Sau đó nó tiếp tục hội thoại với LLM, và message được cập nhật bằng câu trả lời của assistant cùng kết quả tool call.
Để trích thông tin tool call mà LLM trả về, ta thêm một hàm hỗ trợ khác lấy mọi thứ cần để thực hiện lời gọi. Thêm code sau vào cuối 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))
}
Khi đã đủ các phần, ta xử lý prompt ban đầu và gọi LLM. Cập nhật hàm main với code sau:
// 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?;
Code này gửi tới LLM prompt ban đầu hỏi tổng hai số, và xử lý phản hồi để gọi tool một cách động.
Tuyệt, bạn đã làm được!
Bài tập
Lấy code từ bài thực hành và bổ sung thêm tool cho server. Sau đó tạo một client có LLM như trong bài, thử với nhiều prompt khác nhau để đảm bảo mọi tool của server đều được gọi động. Cách xây client này mang lại trải nghiệm tuyệt vời cho người dùng cuối: họ dùng prompt thay vì lệnh client chính xác, và không cần biết có MCP server nào đang được gọi.
Lời giải: solution.
Điểm chính
- Thêm LLM vào client mang lại cách tốt hơn để người dùng tương tác với MCP server.
- Bạn cần chuyển phản hồi của MCP server sang thứ LLM hiểu được.
Mẫu code
- Java Calculator
- .NET Calculator
- JavaScript Calculator
- TypeScript Calculator
- Python Calculator
- Rust Calculator
Bài tiếp theo: Dùng MCP server trong Visual Studio Code.
Nguồn
- Dịch và biên tập từ microsoft/mcp-for-beginners · 03-GettingStarted/03-llm-client/README.md, giấy phép MIT.