MCP for Beginners 实战:为 MCP 客户端接入 LLM,用自然语言驱动服务器工具调用
MCP for Beginners 实战:为 MCP 客户端接入 LLM,用自然语言驱动服务器工具调用
本文基于 mcp-for-beginners 开源课程中《Creating a client with LLM》一课,讲解如何在前几课构建的 MCP 客户端基础上接入一个 LLM(大语言模型),让最终用户通过自然语言提示词即可驱动 MCP 服务器上的工具、资源和提示词,而不是依赖显式的客户端命令。文中给出 TypeScript、Python、.NET、Java、Rust 五种语言的完整可运行示例,读完你将掌握“连接服务器 → 列出能力 → 将能力转换为 LLM 工具格式 → 处理用户提示词并闭环工具调用”的完整链路,以及每种语言对应的仓库源码级参考实现。
为什么客户端需要接入 LLM
到目前为止的课程中,你已经学会了如何创建 MCP 服务器和客户端。此前的客户端是“显式调用”式的:客户端主动调用服务器接口,列出它的工具(tools)、资源(resources)和提示词(prompts)。但这种做法非常不实用——你的用户生活在智能体(Agentic)时代,他们期望直接使用提示词与 LLM 交流,并不关心你底层是否用 MCP 存储能力,他们只希望用自然语言进行交互。
解决办法就是:在客户端中加入一个 LLM。接入 LLM 后,用户输入自然语言提示词,由 LLM 结合客户端列出的服务器能力决定调用哪个工具、传什么参数,最终把结果组织成自然语言回复给用户。这显著提升了客户端侧的最终用户体验。
本课的配套背景说明(原文档开头的 NOTE):Java 客户端示例通过遗留的 HTTP+SSE 传输方式连接,目标是 MCP 2025-11-25 SDK API;新建远程客户端应使用 2026-07-28 兼容的 SDK 与 Streamable HTTP。
整体设计:四步让客户端“听懂人话”
课程给出的客户端与服务器交互方式分四步:
- 与服务器建立连接(establish connection with server)。
- 列出服务器的能力(capabilities)、提示词(prompts)、资源(resources)和工具(tools),并保存它们的 schema。
- 加入一个 LLM,把保存的能力及 schema 以 LLM 能理解的格式传递给它。
- 处理用户提示词:把提示词连同客户端列出的工具一起交给 LLM,由 LLM 决定并触发工具调用。
这四步构成了本课练习(Exercise)的主体骨架,下面逐语言展开。
前置准备:认证与模型接入
GitHub Personal Access Token 的创建步骤
TypeScript / Python / .NET / Rust 示例最初都通过 GitHub 托管的推理端点(https://models.inference.ai.azure.com)调用模型,因此需要先创建一个 GitHub Personal Access Token(PAT),并为其授予 Models 权限。创建流程如下:
- 进入 GitHub 设置:点击右上角头像,选择 Settings;
- 进入开发者设置:向下滚动并点击 Developer settings;
- 选择 Personal Access Token:点击 Fine-grained tokens,然后点击 Generate new token;
- 配置 Token:为便于追溯添加一个说明(note),设置过期时间(expiration),勾选所需权限(scope)——本例必须勾选 Models 权限;
- 生成并复制 Token:点击 Generate token,生成后立即复制保存,因为之后无法再次查看该 Token 值。
Microsoft Foundry 配置说明(权威英文版说明)
需要特别说明的是:英文权威版文档指出,GitHub Models 已于 2026 年 7 月 30 日退役。因此英文原版课程已改用 Microsoft Foundry:创建一个 Foundry 资源,部署一个活跃模型(如 gpt-5.1),并设置以下环境变量:
export AZURE_OPENAI_ENDPOINT="https://<resource-name>.openai.azure.com"
export AZURE_OPENAI_API_KEY="<api-key>"
export AZURE_OPENAI_DEPLOYMENT="gpt-5.1"
调用 API 时使用的是部署名(deployment name),它可能不同于底层模型名。选择模型前应先查阅模型退役时间表。当你实际动手运行本课示例时,建议优先采用这套环境变量(仓库内 03-GettingStarted/03-llm-client/solution/ 下的最新解决方案代码均已迁移到 AZURE_OPENAI_* 变量)。
Java 的 MiniMax 配置
Java 示例不走 GitHub 端点,而是通过 LangChain4j 接入 OpenAI 兼容的 MiniMax API。设置 API Key,可选设置端点和模型:
export OPENAI_API_KEY=your_minimax_api_key_here
export OPENAI_BASE_URL=https://api.minimax.io/v1
export MINIMAX_MODEL_ID=MiniMax-M3
其中 MINIMAX_MODEL_ID 支持 MiniMax-M3 与 MiniMax-M2.7;若未设置 OPENAI_BASE_URL,可用 MINIMAX_REGION 按区域选择端点,支持 global_en(https://api.minimax.io/v1)与 cn_zh(https://api.minimaxi.com/v1):
unset OPENAI_BASE_URL
export MINIMAX_REGION=cn_zh
第 1 步:连接 MCP 服务器
先创建客户端骨架,同时初始化 LLM 客户端和 MCP 客户端两个成员。
TypeScript
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: {}
}
}
);
}
}
这段代码做了三件事:
- 导入所需库(MCP SDK 客户端、stdio 传输、OpenAI 客户端、zod 用于 schema 校验);
- 创建包含
client与openai两个成员的类,分别负责管理 MCP 客户端与对接 LLM; - 配置 OpenAI 客户端实例:
baseURL指向 GitHub 推理 API(https://models.inference.ai.azure.com),apiKey取环境变量GITHUB_TOKEN。
权威英文版对应的构造器会从 AZURE_OPENAI_ENDPOINT / AZURE_OPENAI_API_KEY 读取配置,并把 baseURL 拼接为 ${endpoint.replace(/\/$/, "")}/openai/v1/,且缺少环境变量时直接抛出异常提示,可参见 TypeScript 解决方案。
Python
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())
这里通过 StdioServerParameters 描述服务器进程(可执行文件为 mcp,参数为 run server.py),stdio_client 建立子进程通道,ClientSession 封装会话,session.initialize() 完成协议握手初始化。
.NET
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);
英文权威版使用 OpenAI 官方 .NET SDK 的 ChatClient 对接 Foundry 部署,并用 McpClient.CreateAsync 基于 stdio 传输创建 MCP 客户端;服务器可执行文件指向 02-client 课程中构建好的服务器产物。孟加拉语翻译版对应代码使用的是 Azure.AI.Inference 的 ChatCompletionsClient + GITHUB_TOKEN(端点 https://models.inference.ai.azure.com),两者思想一致,读者可按所选模型服务二选一。
Java
Java 示例基于 LangChain4j。首先在 pom.xml 中添加依赖:
<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>
接着创建 Java 客户端类(完整实现见 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;
}
}
这段代码展示了 LangChain4j 的接入要点:
- 添加 LangChain4j 依赖:MCP 集成与 OpenAI 兼容 MiniMax API 所必需;
- 创建
ChatLanguageModel:用 MiniMax 配置,含 API Key、端点与受支持的模型 ID(MiniMax-M3/MiniMax-M2.7),未设置环境变量时抛出明确错误; - 设置 HTTP 传输:使用 Server-Sent Events(SSE)连接 MCP 服务器(
http://localhost:8080/sse),并可开启请求/响应日志; - 创建 MCP 客户端:
DefaultMcpClient负责与服务器通信; - 利用 LangChain4j 内置 MCP 支持:大大简化 LLM 与 MCP 服务器之间的集成。
Rust
Rust 示例假设你已经有一个基于 Rust 的 MCP 服务器在运行。如果没有,请先回到 01-first-server 课程创建服务器。然后在服务器同级目录下新建 LLM 客户端项目:
mkdir calculator-llmclient
cd calculator-llmclient
cargo init
在 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"] }
注:OpenAI 没有官方 Rust 库,
async-openaicrate 是一个社区维护、被广泛使用的库。
把 src/main.rs 替换为以下基础代码:
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(())
}
这段代码通过 async-openai 的 OpenAIConfig 配置了 OpenAI 兼容客户端(中文翻译版指向 https://models.github.ai/inference/chat;权威英文版改用 AZURE_OPENAI_ENDPOINT 拼接 /openai/v1,完整实现见 Rust 解决方案),并用 rmcp 的 TokioChildProcess 以 cargo run 拉起服务器子进程。
重要:运行应用前务必设置好包含 GitHub Token 的
OPENAI_API_KEY环境变量(权威英文版则为AZURE_OPENAI_ENDPOINT、AZURE_OPENAI_API_KEY、AZURE_OPENAI_DEPLOYMENT)。
第 2 步:列出服务器能力
连接服务器后,需要查询其暴露的能力(工具、资源等),为后续转换做准备。
TypeScript
在同个类中追加以下方法:
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();
}
connectToServer:建立连接并触发run;run:负责应用主流程,目前只列出工具,稍后会继续扩展。
Python
# 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"])
这里列出了资源与工具并打印;对工具还打印了 inputSchema(后面转换时会用到)。
.NET
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;
}
列出了 MCP 服务器上可用的工具;对每个工具打印名称、描述与 schema,schema 稍后将用于调用工具。
Java
// 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
McpToolProvider 自动发现并注册 MCP 服务器上的全部工具,并在内部完成 MCP 工具 schema 与 LangChain4j 工具格式之间的转换,省去了手工列工具和转换的步骤。
Rust
获取 MCP 服务器工具使用 list_tools 方法。在 main 函数中、MCP 客户端设置完成后添加:
// Get MCP tool listing
let tools = mcp_client.list_tools(Default::default()).await?;
第 3 步:把服务器能力转换成 LLM 能理解的工具格式
列出服务器能力后的下一步,是把它们转换为 LLM 能理解的格式——即 OpenAI Function Calling 风格的 type: "function" 工具定义,然后把它们作为 tools 提供给 LLM。
TypeScript
- 添加转换函数
openAiToolAdapter:
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,
},
},
};
}
它把 MCP 服务器返回的工具响应转换为 LLM 可理解的工具定义:基于 input_schema 创建 zod schema,显式设置 type: "function",并把 properties 与 required 映射到 OpenAI 的参数结构。
- 更新
run方法,遍历工具列表并为每个条目调用openAiToolAdapter:
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,
});
});
}
Python
- 创建转换函数
convert_to_llm_tool:
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
- 在客户端代码中调用它,把 MCP 工具响应转成后续可喂给 LLM 的内容:
functions = []
for tool in tools.tools:
print("Tool: ", tool.name)
print("Tool", tool.inputSchema["properties"])
functions.append(convert_to_llm_tool(tool))
.NET
- 添加
ConvertFrom转换函数(权威英文版实现):
ChatTool ConvertFrom(string name, string description, JsonElement jsonElement)
{
return ChatTool.CreateFunctionTool(
functionName: name,
functionDescription: description,
functionParameters: BinaryData.FromString(jsonElement.GetRawText()));
}
该函数接收名称、描述与输入 schema,把 MCP 工具的 JSON schema 直接包装成 OpenAI 的 ChatTool。
- 更新
GetMcpTools循环体,对每个工具调用ConvertFrom并收集结果:
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;
}
工具响应的 input schema 位于 properties 属性中,需要从中提取;之后用工具详情调用 ConvertFrom 完成转换(孟加拉语翻译版中对应的转换实现为 FunctionDefinition + ChatCompletionsToolDefinition,将 schema 包成 { Type = "object", Properties = jsonElement } 再创建工具定义,思路一致)。
Java
// 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();
- 定义了一个极简的
Bot接口,用于自然语言交互; - 用 LangChain4j 的
AiServices把 LLM 与 MCP 工具提供者自动绑定; - 框架在后台自动处理工具 schema 转换与函数调用;
- 无需手工转换工具——LangChain4j 承担了把 MCP 工具转为 LLM 兼容格式的全部复杂度。
Rust
添加 format_tools 辅助函数,把工具列表序列化为 LLM 请求所需的格式(放在 main 函数下方,调用 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)
}
它遍历工具数组,提取 name、description 与 inputSchema,映射成 OpenAI 函数调用格式;properties/required 缺失时使用空对象/空数组兜底。
第 4 步:处理用户提示词并闭环工具调用
这是闭环的关键:把用户提示词发给 LLM,解析响应中的 tool_calls,调用对应 MCP 工具,再把结果组织成最终答复。
TypeScript
- 添加
callTools方法,解析 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
}
}
callTools 接收 LLM 响应,遍历其中的工具调用:取出 function.name 与 function.arguments(字符串形式的 JSON),JSON.parse 后通过 client.callTool 调用服务器工具,并打印结果。
- 更新
run方法,加入 LLM 调用与callTools:
// 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-4.1-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);
}
});
流程为:构造用户消息 → 携带 tools 调用 OpenAI Chat Completions → 遍历每个 choice 的 message,检查是否存在 tool_calls,存在则触发 callTools。权威英文版此处使用 model: process.env.AZURE_OPENAI_DEPLOYMENT ?? "gpt-5.1" 与 max_completion_tokens,对应 Foundry 部署名。
完整 TypeScript 代码(与 解决方案 一致):
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", // may change in future to: 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-4.1-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);
}
});
}
}
let client = new MyClient();
const transport = new StdioClientTransport({
command: "node",
args: ["./build/index.js"]
});
client.connectToServer(transport);
Python
- 添加调用 LLM 所需的导入:
# llm
import os
from openai import OpenAI
import json
- 添加
call_llm函数(权威英文版使用 Foundry 部署gpt-5.1;孟加拉语翻译版对应使用 GitHub Models 端点):
# 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_call
这段代码:把从 MCP 服务器发现并转换好的 functions 传给 LLM → 调用 LLM → 检查返回结果中需要调用哪些函数 → 返回待调用的函数列表(名称 + 已解析的参数)。孟加拉语翻译版中的 call_llm 使用的是 azure.ai.inference.ChatCompletionsClient + GITHUB_TOKEN + 模型 gpt-4o,并带有 temperature=1.、max_tokens=1000、top_p=1. 等可选参数,二者等价,按所选服务端二选一即可。
- 更新主代码,调用 LLM 并执行建议的工具:
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)
这里用 session.call_tool 执行 LLM 基于提示词建议调用的工具,并打印 MCP 服务器返回的结果。完整实现见 Python 解决方案。
.NET
- 发起 LLM 提示词请求(权威英文版):
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;
- 从 MCP 服务器拉取工具:
var tools = await GetMcpTools(); - 定义用户提示词
userMessage; - 构造
ChatCompletionOptions指定模型与工具; - 向 LLM 发起请求。
孟加拉语翻译版对应代码使用 ChatCompletionsOptions(含 Model = "gpt-4.1-mini")与 CompleteAsync。
- 检查 LLM 是否要求调用函数,若需要则调用 MCP 工具:
// 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);
}
}
遍历函数调用列表,解析工具名与参数,用 MCP 客户端调用服务器工具,最后打印文本结果(孟加拉语翻译版取文本的写法为 result.Content.OfType<TextContentBlock>().First().Text)。
完整 C# 代码(与 .NET 解决方案 一致):
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}");
Java
用自然语言提示词直接驱动工具,LangChain4j 在后台完成一切:
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();
}
要点:
- 用简单的自然语言提示词与 MCP 服务器工具交互;
- LangChain4j 框架自动处理:需要时把用户提示词转换为工具调用、根据 LLM 决策调用合适的 MCP 工具、管理 LLM 与 MCP 服务器之间的对话流;
bot.chat()返回的自然语言回复中可能包含 MCP 工具执行结果;- 用户无需了解底层 MCP 实现细节,体验无缝。
Java 完整代码示例(LangChain4jClient,见 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();
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;
}
}
Rust:多轮对话闭环
Rust 部分承担了最多的工作:先用初始用户提示词调用 LLM,再处理响应判断是否需要调用工具;如果需要,就执行工具并把结果带回对话,如此循环直到 LLM 给出最终答复。
定义统一的 LLM 调用函数(权威英文版从 AZURE_OPENAI_DEPLOYMENT 读取模型,缺省 gpt-5.1;孟加拉语翻译版直接指定 openai/gpt-4.1):
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)
}
LLM 响应包含 choices 数组,需要检查其中是否存在 tool_calls——这表示 LLM 请求用特定参数调用某个工具。定义 process_llm_response 处理响应:
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(())
}
存在 tool_calls 时:提取工具信息 → 通过 mcp_client.call_tool 调用 MCP 服务器 → 把工具结果以 role: "tool" 消息加入会话 → 递归继续与 LLM 对话,直到没有更多工具调用、得到最终答复。
再添加一个辅助函数提取 LLM 返回的工具调用信息:
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))
}
最后在 main 函数中串起整个流程:
// 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?;
完整实现见 Rust 解决方案:call_llm 先以初始提示词(求两个数之和)询问 LLM,process_llm_response 动态处理工具调用并持续对话。
仓库中的现成解决方案
本课在 solution 目录 下提供了五套语言的开箱即用实现,可直接对照阅读:
- TypeScript 客户端:
MyClient类完整实现连接、转换、调用闭环; - Python 客户端:含
call_llm、convert_to_llm_tool与主流程; - .NET 客户端:从
GetMcpTools到CallToolAsync的完整串联; - Java 客户端:LangChain4j 的
AiServices+McpToolProvider组合; - Rust 客户端:
call_llm/process_llm_response/format_tools/extract_tool_call_info四个函数协作完成多轮工具调用。
练习、关键要点与延伸阅读
练习(Assignment):沿用练习中的代码,给服务器补充更多工具;然后像练习中那样创建带 LLM 的客户端,用不同的提示词测试,确保服务器所有工具都能被动态调用。这种构建客户端的方式让最终用户获得极佳体验——他们用提示词而非精确的客户端命令交互,且完全感知不到背后有 MCP 服务器被调用。
关键要点(Key Takeaways):
- 在客户端中加入 LLM,为用户提供了与 MCP 服务器交互的更好方式;
- 必须把 MCP 服务器的响应转换为 LLM 能理解的格式。
语言示例(Samples):本课配套了多语言计算器示例,可继续研读:
- Java Calculator
- .NET Calculator
- JavaScript Calculator
- TypeScript Calculator
- Python Calculator
- Rust Calculator
下一步:继续学习 使用 Visual Studio Code 消费服务器(04-vscode),了解如何在 IDE 中直接使用 MCP 服务器。