MCP for Beginners 实战:为 MCP 客户端接入 LLM,用自然语言驱动服务器工具调用

原创2026-10-07 23:57:141,601 阅读
文章标签:教程文档人工智能

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。

整体设计:四步让客户端“听懂人话”

课程给出的客户端与服务器交互方式分四步:

  1. 与服务器建立连接(establish connection with server)。
  2. 列出服务器的能力(capabilities)、提示词(prompts)、资源(resources)和工具(tools),并保存它们的 schema。
  3. 加入一个 LLM,把保存的能力及 schema 以 LLM 能理解的格式传递给它。
  4. 处理用户提示词:把提示词连同客户端列出的工具一起交给 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-openai crate 是一个社区维护、被广泛使用的库。

把 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

  1. 添加转换函数 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 的参数结构。

  1. 更新 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

  1. 创建转换函数 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
  1. 在客户端代码中调用它,把 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

  1. 添加 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。

  1. 更新 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

  1. 添加 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 调用服务器工具,并打印结果。

  1. 更新 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

  1. 添加调用 LLM 所需的导入:
# llm
import os
from openai import OpenAI
import json
  1. 添加 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. 等可选参数,二者等价,按所选服务端二选一即可。

  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

  1. 发起 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。

  1. 检查 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):本课配套了多语言计算器示例,可继续研读:

下一步:继续学习 使用 Visual Studio Code 消费服务器(04-vscode),了解如何在 IDE 中直接使用 MCP 服务器。

登录后查看全文
mcp-for-beginners