使用 Python 入门 Model Context Protocol(MCP)——构建客户端

要消费(使用)一个 MCP 服务器,你需要某种形式的客户端 。例如,你可以使用 Claude DesktopVisual Studio Code(VS Code) 这样的应用,它们能够消费 MCP 服务器、完成特性发现并加以使用。也有一些场景你会需要自己编写客户端------典型例子是把 AI 能力 内建到你的应用中。想象一下,你有一个电商应用,想要一个 AI 增强的搜索:MCP 服务器可以是一个独立应用,而客户端则内置在电商应用里。

基于此,我们来探讨如何构建客户端,以及其中涉及到的内容。

在本章中,你将学会:

  • 同时使用 STDIOSSE 传输构建客户端
  • 消费一个 MCP 服务器及其特性
  • 利用 LLM 提升客户端体验

本章涵盖以下主题:

  • 构建客户端
  • 练习:构建客户端
  • 带有 LLM 的客户端
  • 与 LLM 协作
  • 练习:整合 LLM

构建客户端

那么,构建客户端要做哪些事?从高层来看,需要:

  1. 让客户端连接到服务器;
  2. 列出特性;
  3. 选择要使用的特性;
  4. 提示用户输入参数;
  5. 展示结果。

理解了这些步骤后,接下来通过一个可跟打的练习来实现它。

练习:构建客户端

在本练习中,你将构建一个连接到服务器并消费其特性的客户端。你将使用 SDK 来构建客户端并调用服务器。该客户端是一个命令行应用:允许你选择特性、提供参数,然后调用服务器并显示结果。

让客户端连接到服务器

先编写用于与服务器建立连接的客户端代码:

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()
            # list features

if __name__ == "__main__":
    import asyncio
    asyncio.run(run())

以上我们完成了:

  • 创建 StdioServerParameters 对象,指定如何启动服务器及可选命令行参数------因为服务器与客户端会同时运行,需要告诉客户端如何拉起服务器;
  • 定义 run 函数,创建 ClientSession 并初始化与服务器的连接。稍后我们会在 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)

这里我们:

  • 列出可用资源:打印每个资源的名称;
  • 列出可用工具:打印每个工具的名称(当然也可以打印描述和输入模式,这里先简化为名称)。

选择并使用一个特性

下面用一个已列出的工具作为示例进行调用。这里使用上一章创建的 add 工具。add 接收两个参数 ab,返回它们的和。设想用户从列表里选中了某个工具,我们再提示用户输入参数并进行调用:

python 复制代码
# Read information from the first tool
tool_name = tools.tools[0].name
print(f"Using tool: {tool_name}")
first_value = input("Enter first value: ")
second_value = input("Enter second value: ")

# Call a tool
print("CALL TOOL")
result = await session.call_tool(tool_name, arguments={
    "a": first_value, "b": second_value})
print(result.content)

以上我们:

  • 读取第一个工具名称并打印;
  • 提示用户输入两个参数;
  • 通过 call_tool 调用该工具,并把参数以字典形式传入,随后打印返回结果。

到这里,我们已经有了一个能连接服务器、列出特性并调用工具的客户端。不过它仍然偏"编程式",用户体验不算友好。

完整代码

在整合 LLM 之前,先看完整客户端代码:

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

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

            # Read information from the first tool
            tool_name = tools.tools[0].name
            print(f"Using tool: {tool_name}")
            first_value = input("Enter first value: ")
            second_value = input("Enter second value: ")

            # Call a tool
            print("CALL TOOL")
            result = await session.call_tool(tool_name, arguments={"a": first_value, "b": second_value})
            print(result.content)

            # Read a resource
            print("READING RESOURCE")
            content, mime_type = await session.read_resource("greeting://hello")

if __name__ == "__main__":
    import asyncio
    asyncio.run(run())

如果你跟着代码一起敲,现在就应当有一个可用客户端:它能连接到服务器并消费其特性。章节末尾的作业会给你更多练习机会。

接下来我们通过整合 LLM 来改进客户端,你将看到这如何带来更佳的用户体验,并帮助抽象服务器使用的复杂度。

带 LLM 的客户端(Clients with LLMs)

到目前为止,你已经看到了如何用 STDIOSSE 构建并测试客户端。不过你可能也注意到,这种方式相当"编程味"而且对用户不够友好------也就是说,每次想用某个能力,都需要知道 特性的精确名称 及其参数 。这正是 LLM 派上用场的地方:把 LLM 引入客户端后,你可以把"知道细节"的负担抽象掉,把注意力放在"完成任务"上。下面看看它如何工作。

在使用 LLM 之前

如果用 LLM,应用的构建与流程通常是:

  1. 列出服务器特性;
  2. 用户选择某个特性,客户端再询问参数;
  3. 对响应做相应处理。

这种方式比较僵硬 ,要求用户必须明确知道并从列表中显式选择某个特性。那么,有没有更好的方式?

引入 LLM 之后

为了解决客户端"僵硬"的问题,设想用户并不了解 这些特性------他们只通过自然语言提示进行交互。此时应用的流程变为:

  1. 列出服务器特性;
  2. 将特性列表转换 为可供 LLM 使用的工具
  3. 用户输入自然语言请求;
  4. 客户端把请求发送给 LLM------LLM 负责判断用哪个特性带哪些参数;如果没有匹配的特性,就返回通用的 LLM 回复。

这种方式在用户体验上差异巨大:用户不必再了解或手动选择特性,只需输入自然语言请求,剩下的由 LLM 来"决定"。

接下来看看实操方式。

使用 LLM(Working with an LLM)

目前有很多 AI 提供商可供调用 LLM。本书使用 GitHub Models (免费),只要你有 GitHub 账号即可。要使用 GitHub Models,你需要在 GitHub Codespaces 中启动项目,或配置一个具有相应权限的 个人访问令牌(PAT) 。之所以需要令牌,是因为你要调用 API,令牌会作为 Bearer Token 用于身份验证。若使用本地模型(如 Ollama ),则不需要令牌。虽然可以在源码里直接写令牌,但出于安全,建议放在环境变量中。

如果你从未使用过 AI,需要知道的核心概念是:向模型发送一个提示(prompt) ,得到一个回复(response) 。提示是描述你希望 LLM 执行何事的自然语言文本;回复也是自然语言文本,包含对该提示的答案。

把 LLM 与 MCP 结合使用的思路是:让 LLM 指示 在给定提示下应调用哪个函数 。例如,当提示是 "Add 1 and 2 " 时,如果 MCP 服务器中存在名为 add 且参数为 ab 的函数,那么 LLM 应该给出"调用 add(a=1, b=2)"的指示。

下面是调用代码示例。我们将调用一个 GitHub Models 的模型,所以请确保你有 GitHub 账号并创建了具有正确权限的 PAT,或在 Codespaces 中运行。

需要明确以下内容:

  • GitHub Models 的端点(endpoint)
  • 使用的模型 (此处为 gpt-4o
  • 要发送的提示(prompt) (可从用户输入收集,或像示例中一样写死)
  • 要让 LLM 使用的函数 (此处是上一章创建的 add 工具)

当然,你也可以只发 prompt、收一段文本答案;但这里我们希望 LLM 指出要调用的函数 及其参数 ,所以会把函数定义 也一并发给模型。函数定义是一个 JSON 对象,描述函数名、说明与参数(参数按 JSON Schema 定义)。

ini 复制代码
# json description of functions
functions = [
    {
        "type": "function",
        "function": {
            "name": "add",
            "description": "Add two numbers",
            "type": "function",
            "parameters": {
                "type": "object",
                "properties": {
                    "a": {
                        "type": "number",
                        "description": "The first number to add"
                    },
                    "b": {
                        "type": "number",
                        "description": "The second number to add"
                    }
                },
                "required": ["a", "b"]
            }
        }
    }
]

# get token from environment variable
token = os.environ["GITHUB_TOKEN"]

# the endpoint for GitHub Models
endpoint = "https://models.github.ai/inference"

# the model to use
model_name = "gpt-4o"

# creation of chat client
client = OpenAI(
    base_url=endpoint,
    api_key=token
)

print("CALLING LLM")
response = client.chat.completions.create(
    messages=[
        {
            "role": "system",
            "content": "You are a helpful assistant.",
        },
        {
            "role": "user",
            "content": prompt,
        },
    ],
    model=model_name,
    tools = functions,
    # Optional parameters
    temperature=1.,
    max_tokens=1000,
    top_p=1.
)
response_message = response.choices[0].message
print("LLM RESPONSE: ", response_message)

另外,除了 prompt,你还可以给 LLM 传一些配置项 ,如 temperaturemax_tokenstop_p 等。这里不展开说明(可参考 OpenAI 文档);简单来说,它们会影响回复的随机性/创造性 以及所谓"上下文窗口"的大小。

练习:集成 LLM(Exercise: Integrating the LLM)

下面看看如何把 LLM 集成到客户端中。目标是显著提升用户体验,并抽象掉使用服务器的复杂性。为此我们需要完成这些步骤:

  • 列出服务器特性:通过列出特性,了解可用能力;
  • 将特性列表转换为 LLM 可用的工具:MCP 服务器返回的特性不能被 LLM 直接使用,需要转换成 LLM 能理解的格式;
  • 管理用户输入:允许用户输入自然语言请求;客户端向 LLM 发起补全请求,由 LLM 告诉我们该用哪个特性以及应传什么参数。

开干!

列出服务器特性

如果你是在消费他人的 MCP 服务器,这可能是你做的第一件事。在继续之前,确保已安装 MCP SDK。

第一步与之前相同:列出服务器可用特性。通过列出工具实现:

python 复制代码
tools = await session.list_tools()
print("LISTING TOOLS")
for tool in tools.tools:
    print("Tool: ", tool.name)

将特性列表转换为 LLM 工具

下一步很关键:把特性列表转换为 LLM 能理解的格式。这样我们才能让 LLM 决定应该用哪个特性。转换代码如下:

先添加转换函数:

arduino 复制代码
def 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

该函数接收一个 tool,并将其转换成 LLM 可理解的格式。

调用转换过程:

css 复制代码
functions = []
for tool in tools.tools:
    print("Tool: ", tool.name)
    print("Tool", tool.inputSchema["properties"])
    functions.append(to_llm_tool(tool))

可以看到,我们遍历工具列表,对每个工具调用转换函数。

现在一切就绪,接下来要管理用户输入并向 LLM 发送补全请求 。LLM 的响应会告诉我们要调用的函数名 及其参数(这里的函数即服务器上的某个特性)。

用户输入自然语言,LLM 发起补全请求

我们需要做两件事:

  1. 调用 LLM;2) 判断 LLM 返回的是函数调用 还是通用文本

调用 LLM:

ini 复制代码
def call_llm(prompt, functions):
    token = os.environ["GITHUB_TOKEN"]
    endpoint = "https://models.github.ai/inference"
    model_name = "gpt-4o"
    client = OpenAI(
        base_url=endpoint,
        api_key=token,
    )
    print("CALLING LLM")
    response = client.complete(
        messages=[
            {
                "role": "system",
                "content": "You are a helpful assistant.",
            },
            {
                "role": "user",
                "content": prompt,
            },
        ],
        model=model_name,
        tools = functions,
        # Optional parameters
        temperature=1.,
        max_tokens=1000,
        top_p=1.  
    )
    response_message = response.choices[0].message
    print("LLM RESPONSE: ", response_message)
    functions_to_call = []

上述我们:

  • 定义了一个函数,接收 promptfunctions,并返回 LLM 响应;
  • 使用 complete 方法调用 LLM,传入提示与函数集合,并打印响应。

检查 LLM 是否返回函数调用:

css 复制代码
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

这里我们:

  • 通过检查 response_message.tool_calls 判断是否存在函数调用;
  • 遍历 tool_calls,打印工具名与参数;
  • 返回待调用函数列表

客户端决定调用哪个服务器特性与参数

此时我们拿到了 LLM 的响应(可能包含函数调用)。下一步就是按需调用 MCP 服务器:

ini 复制代码
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)

我们做了:

  • 调用 LLM,得到待调用的函数集合;
  • 逐个调用 MCP 服务器上的工具(call_tool),并打印结果。

很不错吧?用户现在可以直接用自然语言与服务器交互了,你也因此极大简化了用户的心智负担。

总结

本章展示了如何构建能够连接 MCP 服务器并消费其特性的客户端。我们先从能列出并调用特性 的简单客户端开始,然后通过集成 LLM显著提升体验:用户以自然语言表达意图,LLM 负责决定调用哪个特性及其参数,从而隐藏底层复杂度。

下一章,我们将介绍如何在 VS CodeClaude Desktop 中消费 MCP 服务器。

相关推荐
数据智能老司机7 小时前
使用 Python 入门 Model Context Protocol(MCP)——构建 SSE 服务器
llm·agent·mcp
Baihai_IDP7 小时前
探讨超长上下文推理的潜力
人工智能·面试·llm
DO_Community7 小时前
裸金属 vs. 虚拟化 GPU 服务器:AI 训练与推理应该怎么选
运维·服务器·人工智能·llm·大语言模型
少林码僧9 小时前
1.1 大语言模型调用方式与函数调用(Function Calling):从基础到实战
人工智能·ai·语言模型·自然语言处理·llm·1024程序员节
扯蛋43817 小时前
LangChain的学习之路( 一 )
前端·langchain·mcp
oe101919 小时前
好文与笔记分享 A Survey of Context Engineering for Large Language Models(上)
数据库·笔记·语言模型·agent·上下文工程
杨成功20 小时前
大语言模型(LLM)学习笔记
人工智能·llm
亚里随笔1 天前
AsyPPO_ 轻量级mini-critics如何提升大语言模型推理能力
人工智能·语言模型·自然语言处理·llm·agentic