首页 / 文章 / 从零开始学习MCP 第3部分:将客户端连接到服务器

从零开始学习MCP 第3部分:将客户端连接到服务器

构建一个MCP标准输入输出客户端,用于启动hr_server.py、调用search_employee函数并返回结果——无需手动启动服务器。

804 词

上一部分留下了一个仅承担单一职责的MCP服务器:提供人力资源搜索功能。

HR MCP Server
      ↓
search_employee

整个流程始于:

python hr_server.py

还有一个问题尚未解决:究竟是谁与服务器进行交互?这个角色由客户端承担。

Client
   ↓
MCP
   ↓
HR MCP Server

简单理解这些概念即可:服务器提供功能,客户端则连接并使用这些功能。

让我们创建客户端

在hr_server.py旁边,再添加一个模块:

hr_client.py

项目的结构变为:

mcp-hr
│
├── hr_server.py
│
└── hr_client.py

既然服务器部分已经编写完成,现在重点就转向客户端。

连接到我们的服务器

将以下内容放入hr_client.py中:

import asyncio

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    server = StdioServerParameters(
        command="python",
        args=["hr_server.py"]
    )
    async with stdio_client(server) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

asyncio.run(main())

乍看之下这段代码似乎很复杂,但关键部分在于:

server = StdioServerParameters(
    command="python",
    args=["hr_server.py"]
)

这些参数用于指示客户端启动并连接到hr_server.py。客户端会启动服务器进程,并与其建立标准输入输出连接。

现在让我们调用我们的工具

第二部分定义了一个名为:

search_employee

客户端可以这样搜索John:

await session.initialize()
result = await session.call_tool(
    "search_employee",
    {"name": "John"}
)

print(result.content[0].text)

用通俗的话来说,客户端会请求该连接运行名为John的search_employee函数。完整的客户端脚本如下:

import asyncio

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    server = StdioServerParameters(
        command="python3",
        args=["hr_server.py"]
    )
    async with stdio_client(server) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.call_tool(
                "search_employee",
                {"name": "John"}
            )
            print(result.content[0].text)

asyncio.run(main())

该文件就是演示中客户端的全部代码。

让我们运行它

在终端中执行:

python hr_client.py

一个有用的细节是:hr_server.py无需手动单独启动。客户端会通过以下方式来启动它:

command="python",
args=["hr_server.py"]

然后连接到正在运行的进程。

会发生什么?

客户端会发送一个类似如下的工具请求:

Tool:
  search_employee
Name:
  John

服务器接收到该请求后执行以下操作:

search_employee("John")

它进行解析:

John → Finance

然后将结果返回给调用方。整个流程的路径如下:

hr_client.py
     │
     │  search_employee("John")
     ↓
hr_server.py
     │
     ↓
Search employee list
     │
     ↓
John → Finance
     │
     ↓
hr_client.py

协议的双方现在都完成了真正的往返通信。

回到我们的USB示例

将键盘连接到计算机是一个很有用的类比。

Keyboard
   ↓
USB
   ↓
Computer

两端都遵循相同的电缆通信协议。在这里,客户端使用MCP,服务器也使用MCP。正是这份共同的协议规范使得它们能够协同工作。

但它真的选择了合适的工具吗?

有一个容易被忽视的局限性。再来看看调用场景:

session.call_tool(
    "search_employee",
    {"name": "John"}
)

谁选择了search_employee?是应用程序的开发者,他通过硬编码工具名称来决定使用哪个功能。客户端从未读取过诸如“约翰在财务部门工作吗?”这样的自然语言问题来决定调用哪个功能。接下来缺失的环节就是自动工具选择功能。

下一步是什么?

假设服务器上增加了多个工具:

search_employee
create_employee
get_leave_balance
list_departments

当用户询问约翰属于哪个部门时,必须有机制来判断应该使用哪个功能:

search_employee

而不是其他类似的功能。这一判断取决于工具名称、描述以及展示的目录信息。接下来的内容将介绍当存在多个MCP工具时,应用程序如何进行选择。

在此之前,这一步骤带来的重要启示在于其机制:stdio客户端即可启动服务器、初始化会话、带参数调用指定工具,并输出结构化回复——无需手动启动独立的服务器进程。