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

原创2026-10-01 15:31:041,649 阅读
文章标签:教程文档人工智能

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

本教程来自开源课程 mcp-for-beginners 的第三章第三课(03-GettingStarted/03-llm-client/README.md)。前面两课你已经学会了创建 MCP Server 和显式调用工具的 MCP Client,本课将把 LLM 加入客户端:客户端先连接 Server 并列出其 capabilities(工具、资源、提示词及 schema),再把这些能力转换成 LLM 能理解的 tool 格式,随后把用户自然语言提示连同工具定义一起交给 LLM,由 LLM 决定调用哪个 MCP 工具,最终把结果返回给用户。学完本课,你将掌握 TypeScript、Python、.NET、Java、Rust 五种语言下"LLM + MCP 工具调用"的完整闭环实现,让终端用户只用自然语言就能使用 MCP Server 的全部能力。

为什么客户端需要 LLM

在之前的课程里,客户端通过显式调用服务端的 API 来列举工具、资源和提示词。这种方式对开发者友好,但对最终用户并不实用:用户生活在"智能体时代"(agentic era),期望直接用自然语言与系统交互,而不关心你的能力是通过 MCP 存储的还是通过其他方式组织的。解决思路非常直接——在客户端里加一个 LLM:

  1. 与服务端建立连接;
  2. 列出 capabilities(prompts、resources、tools),并保存它们的 schema;
  3. 引入 LLM,把保存的 capabilities 及其 schema 转换成 LLM 能理解的形式;
  4. 处理用户提示:把用户提示连同客户端列出的工具一起交给 LLM,由 LLM 决定并驱动工具调用。

整个过程的本质是:MCP 负责"能力发现与执行",LLM 负责"意图理解与调度",二者通过 OpenAI 兼容的 function calling(工具调用)协议衔接。

课前准备:配置 Microsoft Foundry

GitHub Models 已于 2026 年 7 月 30 日退役。本课示例使用 Microsoft Foundry 作为 LLM 后端,需要先创建 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),它可能与底层模型名不同;
  • 选型前请查阅 Microsoft Foundry 的模型退役时间表(model retirement schedule),避免使用即将退役的模型;
  • TypeScript / Python / .NET / Rust 示例均以这组 AZURE_OPENAI_* 环境变量为准(AZURE_OPENAI_DEPLOYMENT 通常提供默认值 gpt-5.1);
  • Java 示例走另一条路线,使用 LangChain4j + OpenAI 兼容的 MiniMax API(见下文)。

第一步:创建并连接客户端

TypeScript

核心思路是用官方 MCP SDK 创建 Client,同时用 openai 官方 SDK 创建指向 Microsoft Foundry v1 端点的 OpenAI 客户端:

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: {}
            }
            );
    }
}

这段代码完成了三件事:导入所需库;创建包含 client(MCP 客户端)与 openai(LLM 客户端)两个成员的类;把 OpenAI 客户端配置到 Microsoft Foundry v1 端点。注意 baseURL 由 AZURE_OPENAI_ENDPOINT 拼出,末尾的 / 会被去掉再补上 /openai/v1/。

Python

使用官方 mcp SDK 的 stdio_client + ClientSession 建立连接:

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())

这里通过 stdio 管道以子进程方式启动 MCP Server(mcp run server.py),ClientSession 封装了与 Server 的 JSON-RPC 会话,await session.initialize() 完成协议握手。

.NET

.NET 侧使用 ModelContextProtocol SDK 的 McpClient.CreateAsync,并通过 StdioClientTransport 指向第二步构建好的 Server 可执行文件:

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);

ChatClient 负责与 LLM 交互(OpenAI.Chat 命名空间),McpClient 负责与 MCP Server 交互;StdioClientTransport 配置了 Server 的命令路径。仓库内的完整版本(solution/dotnet/Program.cs)把 Server 路径改成了基于 AppContext.BaseDirectory 的相对解析,移植性更好。

Java(LangChain4j + MiniMax)

Java 示例采用 LangChain4j 框架,先在 pom.xml 中加入 MCP 集成与 OpenAI 兼容 API 的依赖:

<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>

接着设置 MiniMax 的 API Key,以及可选的端点与模型环境变量。MINIMAX_MODEL_ID 支持 MiniMax-M3 与 MiniMax-M2.7;当未设置 OPENAI_BASE_URL 时,MINIMAX_REGION 支持 global_en 与 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

也可以按区域选择端点,此时省略 OPENAI_BASE_URL:

unset OPENAI_BASE_URL
export MINIMAX_REGION=cn_zh

创建客户端类:

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;
    }
}

这段代码的要点:

  • 通过 OpenAiOfficialChatModel.builder() 构建 Chat 模型,resolveBaseUrl()/resolveModelName() 负责解析环境变量并提供清晰的参数校验错误;
  • 通过 HttpMcpTransport(SSE 传输)连接 MCP Server,并可开启 logRequests/logResponses 调试日志;
  • DefaultMcpClient 负责与 Server 通信;
  • 完整代码见 solution/java/src/main/java/com/microsoft/mcp/sample/client/LangChain4jClient.java。

Rust

Rust 示例假设你已有一个 Rust 编写的 MCP Server(如果没有,请先完成 01-first-server 一课)。在与 Server 相同的目录下新建客户端项目:

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"] }

[!NOTE] 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 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(())
}

[!IMPORTANT] 运行前必须设置 AZURE_OPENAI_ENDPOINT、AZURE_OPENAI_API_KEY 和 AZURE_OPENAI_DEPLOYMENT 三个环境变量。

完整实现中(solution/rust/src/main.rs)Server 目录改为从 CARGO_MANIFEST_DIR 向上回溯解析到 01-first-server/solution/rust。

第二步:列出 Server capabilities

连接建立后,需要向 Server 询问它提供了哪些能力。

TypeScript

添加连接方法与 run 方法,目前 run 只列出工具:

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();
}

Python

ClientSession 上直接有 list_resources() 与 list_tools():

# 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,稍后转换 LLM 工具时要用到。

.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;
}

逐个打印工具的名称、描述和 JSON schema——schema 正是下一步转换的原料。

Java

LangChain4j 用 McpToolProvider 自动发现 MCP 工具,省去了手动列举的步骤:

// 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 Server 的工具、把 MCP 工具 schema 转换为 LangChain4j 格式、管理工具执行与响应——把手工列举与转换的复杂性抽象掉了。

Rust

通过 list_tools 方法获取工具列表:

// Get MCP tool listing
let tools = mcp_client.list_tools(Default::default()).await?;

第三步:把 Server capabilities 转换成 LLM 工具

MCP 返回的工具定义是 JSON Schema 形态,而 LLM 的 function calling 需要 OpenAI 风格的 {"type": "function", "function": {...}} 结构,因此需要一个转换适配层。

TypeScript

  1. 添加 openAiToolAdapter,把 MCP 工具响应转换为 LLM 可用的工具定义:

    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,
            },
            },
        };
    }
    
  2. 更新 run 方法,把 listTools() 的结果逐项映射为 LLM 工具:

    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
    
  2. 在客户端代码中循环调用该函数,收集 LLM 能理解的 functions 列表:

    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 把 MCP 工具的 JSON schema 转成 OpenAI ChatTool:

    ChatTool ConvertFrom(string name, string description, JsonElement jsonElement)
    {
        return ChatTool.CreateFunctionTool(
            functionName: name,
            functionDescription: description,
            functionParameters: BinaryData.FromString(jsonElement.GetRawText()));
    }
    
  2. 在 GetMcpTools 中调用它,并注意:输入 schema 位于工具响应的 properties 属性上,需要先提取:

    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;
    }
    

Java

定义 Bot 接口并用 AiServices 把 LLM 与工具提供者绑定——工具转换与函数调用全部由框架接管:

// 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();

Rust

添加 format_tools 辅助函数,把 ListToolsResult 序列化为 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)
}

至此,客户端已具备把 MCP 能力"翻译"给 LLM 的能力,可以开始处理用户请求了。

第四步:处理用户提示,让 LLM 驱动工具调用

TypeScript

  1. 添加 callTools 方法:遍历 LLM 返回的 tool calls,用 client.callTool 真正调用 MCP 工具:

    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
    
        }
    }
    
  2. 更新 run 方法:构造消息、携带 tools 调用 LLM、遍历响应的 choices 检查 tool_calls:

    // 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);
        }
    });
    

完整代码(solution/typescript/src/client.ts)将上述步骤组装成一个 MyClient 类,并在文件末尾通过 StdioClientTransport 启动:

let client = new MyClient();
 const transport = new StdioClientTransport({
            command: "node",
            args: ["./build/index.js"]
        });

client.connectToServer(transport);

运行时输出大致为(见 solution/typescript/README.md):

Asking server for available tools
MCPClient started on stdin/stdout
Querying LLM:  What is the sum of 2 and 3?
Making tool call
Calling tool add with args "{\"a\":2,\"b\":3}"
Tool result:  { content: [ { type: 'text', text: '5' } ] }

Python

  1. 增加调用 LLM 所需的导入:

    # llm
    import os
    from openai import OpenAI
    import json
    
  2. 定义 call_llm 函数(默认使用活跃模型 gpt-5.1,选型前请查阅 Foundry 模型退役时间表):

    # 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
    

    call_llm 把从 MCP Server 发现并转换好的 functions 传给 LLM,随后检查响应中的 tool_calls,返回需要调用的函数列表。

  3. 更新主流程:询问 LLM 该调用哪些工具,然后用 session.call_tool 真正执行:

    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)
    

    运行输出示例(见 solution/python/README.md):

    LISTING RESOURCES
    Resource:  ('meta', None)
    Resource:  ('nextCursor', None)
    Resource:  ('resources', [])
                    INFO     Processing request of type ListToolsRequest                                                                               server.py:534
    LISTING TOOLS
    Tool:  add
    Tool {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
    CALLING LLM
    TOOL:  {'function': {'arguments': '{"a":2,"b":20}', 'name': 'add'}, 'id': 'call_BCbyoCcMgq0jDwR8AuAF9QY3', 'type': 'function'}
    [05/08/25 21:04:55] INFO     Processing request of type CallToolRequest                                                                                server.py:534
    TOOLS result:  [TextContent(type='text', text='22', annotations=None)]
    

.NET

  1. 先取回工具,构造 ChatCompletionOptions 并调用 LLM:

    var tools = await GetMcpTools();
    
    for (int i = 0; i < tools.Count; i++)
    {
        var tool = tools[i];
        Console.WriteLine($"MCP Tools def: {i}: {tool}");
    }
    
    // 0. Define the chat history and the user message
    var userMessage = "add 2 and 4";
    
    chatHistory.Add(new 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;
    
  2. 检查响应中的函数调用,逐项解析参数并回调 MCP Server:

    // 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);
        }
    
    }
    

    CallToolAsync 返回的结果中,TextContentBlock 承载了工具执行的文本结果;完整程序见 solution/dotnet/Program.cs,其中还包含系统提示词(SystemChatMessage)与最终输出 Assistant response 的打印。

Java

直接通过 bot.chat() 用自然语言提问,框架自动完成"意图 → 工具调用 → 结果回填"的循环:

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();
}

LangChain4j 自动处理三件事:把用户提示转换为必要的工具调用;根据 LLM 的决策调用合适的 MCP 工具;管理 LLM 与 MCP Server 之间的对话流。bot.chat() 返回自然语言响应,其中可能包含 MCP 工具执行的结果——用户完全感知不到底层 MCP 的存在。完整可运行版本见 LangChain4jClient.java(含 Bot 接口定义)。

Rust:完整的工具调用循环

Rust 的第四步最完整地展示了"LLM ↔ 工具执行 ↔ 继续对话"的递归循环。

  1. 定义 call_llm 函数,携带消息与格式化后的工具列表请求 LLM:

    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)
    }
    
  2. 定义 process_llm_response:解析 choices[].message,若存在 tool_calls,把助手消息加入对话、逐个调用 MCP 工具、把工具结果以 role: "tool" 消息回填,然后递归续谈,直到 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(())
    }
    
  3. 定义 extract_tool_call_info,从 LLM 返回的 tool call 中提取 tool_id、工具名与参数 JSON:

    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))
    }
    
  4. 在 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?;
    

    process_llm_response 通过 Box::pin(...).await 实现自递归,这一模式正是多轮 Agent 式工具调用的标准骨架;完整实现见 solution/rust/src/main.rs。

练习与作业

动手实践:把 01-first-server 的 Server 扩展出更多工具,然后仿照本课练习编写一个带 LLM 的客户端,用不同的自然语言提示测试,确保 Server 上的每个工具都能被动态调用。这种构建方式意味着终端用户可以使用提示词而非精确的命令来操作,甚至完全感知不到底层 MCP Server 的存在,用户体验大幅提升。

各语言完整可运行示例位于 solution 目录:

仓库的 samples 目录还提供了各语言的 Calculator 服务端示例(Java、.NET、JavaScript、TypeScript、Python、Rust),可作为扩展 Server 工具的参照。

关键要点

  • 给客户端接入 LLM,是让用户以自然语言方式与 MCP Server 交互的更好途径;
  • 必须把 MCP Server 返回的工具定义转换成 LLM 能理解的格式(OpenAI function calling 风格);
  • 转换完成后,LLM 依据用户提示决定调用哪个工具,客户端负责把调用转发给 MCP Server 并取回结果,最终以自然语言返回给用户。

下一步

本课实现的是基于标准 MCP SDK 的 LLM 客户端。接下来可以学习如何用 Visual Studio Code 消费 MCP Server:04-vscode。

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