手搓MCP客户端&服务端:从零到实战极速了解MCP是什么?

大家好,我是阿坡,今天我通过极简单的案例,带大家手搓一个MCP客户端和服务端代码,来快速了解MCP是什么?

MCP是一个典型的CS架构,对于有编程基础的同学来说,很容易理解,因为开发中常见的MySql就是典型的CS架构,程序员日常开发过程中,会经常接触到CS架构的产品。

本文不做太多关于MCP是什么的解释,尽最可能减少无关的噪音,只需要知道,MCP(Model Context Protocol)是一个标准化协议,通过客户端-服务端架构,让AI模型能够安全地调用工具、访问外部数据源,并实时获取信息。

这个案例的内容就是:让AI根据你的输入自动规划并调用MCP服务端,给本地电脑创建一个文件,并写入一句话。

如果这个案例你跑通了,你就会对MCP有一个初步的且正确的认知了。后面,你再去看网上其他关于MCP的大段文字科普,或者眼花缭乱的客户端配置MCP服务器的教程,再或者通过Dify等工作流与MCP结合,就会有种拨云见日,一览众山小的感觉。

一、环境安装

一)下载并安装python

官网:www.python.org/

二)安装uv

1、uv介绍

MCP开发要求借助uv进行虚拟环境创建和依赖管理。uv 是一个Python 依赖管理工具,采用 Rust 编写,功能类似于pip,venv,但它更快、更高效,并且可以更好地管理 Python 虚拟环境和依赖项,也就是说他兼有了创建虚拟环境和包管理工具的功能,可以平替pip,venv。

它完全兼容 pip :支持 requirements.txt 和 pyproject.toml 依赖管理。 跨平台:支持 Windows、macOS 和 Linux。

2、uv安装

ctrl+r,打开命令行,输入一下命令安装uv

复制代码
pip install uv

uv常见使用命令可以自行问AI或百度,此处不再赘述。

二、案例场景概述

我们先从最简单的案例入手,创建client端,server端,然后联调通,对吗,mcp建立起一个初步完整的认知即可,尽最大可能避免引入复杂的东西。

所以,我们的案例就是:通过手动创建的client运行起来后,来调用server端的逻辑,server端的逻辑就是:创建一个名为aaa.txt文件,写入 今天天气真好! ,即可。

三、MCP客户端

一)初始化client项目

进入自己的代码目录下,创建一个文件夹:

bash 复制代码
# 创建并初始化项目目录
uv init mcp_client

# 进入文件夹
cd mcp_client

可以看到一个完整的初始化项目目录:包含项目入口文件,依赖管理文件等

二)创建MCP客户端虚拟环境

这里我们用cursor打开项目,进行后面的操作。

我们选择Command Prompt 来执行后面的命令:

bash 复制代码
# 创建虚拟环境
uv venv 

# 激活虚拟环境
# On Windows:
.venv\Scripts\activate
# On Unix or MacOS:
source .venv/bin/activate

这里,uv会自动识别当前项目主目录,然后自动创建虚拟环境。

现在,我们通过add方法在虚拟环境中安装相关的库。

注意:如果网络问题,安装依赖失败,可以更改镜像源,或者使用科学上网工具。

csharp 复制代码
# 安装 MCP依赖包
uv add mcp

三)新增client所需依赖

后面,为了支持调用大模型,读取env环境变量中的API_KEY等信息,需要先安装如下依赖:

csharp 复制代码
uv add mcp openai python-dotenv

四)接入DeepSeek在线大模型

1、创建env文件

env文件用来存放大模型API Key等配置信息:

ini 复制代码
BASE_URL=https://api.deepseek.com
MODEL=deepseek-chat
API_KEY="你的API_KEY"

2、编写MCP客户端

具体也可以参考官方示例:modelcontextprotocol.io/quickstart/...

客户端代码如下:

python 复制代码
import asyncio
import os
from openai import OpenAI
from dotenv import load_dotenv
from contextlib import AsyncExitStack

# 加载 .env 文件
load_dotenv()

class MCPClient:

    def __init__(self):
        """初始化 MCP 客户端"""
        self.exit_stack = AsyncExitStack()
        self.openai_api_key = os.getenv("API_KEY")  # 读取 OpenAI API Key
        self.base_url = os.getenv("BASE_URL")  # 读取 BASE URL
        self.model = os.getenv("MODEL")  # 读取 model

        if not self.openai_api_key:
            raise ValueError("未找到 API KEY. 请在.env文件中配置API_KEY")

        self.client = OpenAI(api_key=self.openai_api_key,
                             base_url=self.base_url)

    async def process_query(self, query: str) -> str:
        """调用 OpenAI API 处理用户问题"""
        messages = [{
            "role": "system",
            "content": "你是一个智能助手,帮助用户回答问题。"
        }, {
            "role": "user",
            "content": query
        }]

        try:
            # 调用 大模型API
            response = await asyncio.get_event_loop().run_in_executor(
                None,
                lambda: self.client.chat.completions.create(model=self.model,
                                                            messages=messages))
            return response.choices[0].message.content
        except Exception as e:
            return f"调用模型API时出错: {str(e)}"

    async def chat_loop(self):
        """运行交互式聊天循环"""
        print("MCP 客户端已启动!输入 'exit' 退出")

        while True:
            try:
                query = input("问: ").strip()
                if query.lower() == 'exit':
                    break

                response = await self.process_query(query)
                print(f"AI回复: {response}")

            except Exception as e:
                print(f"发生错误: {str(e)}")

    async def clean(self):
        """清理资源"""
        await self.exit_stack.aclose()


async def main():
    client = MCPClient()
    try:
        await client.chat_loop()
    finally:
        await client.clean()


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

3、运行client

arduino 复制代码
uv run client.py

五)接入ollama本地大模型

大模型部署的详细教程,这里不再赘述,可以参考之前我写的文章:DeepSeek + Dify :零成本搭建企业级本地私有化知识库保姆级喂饭教程

1、确保ollama已启动

没有启动的,输入如下命令来启动ollama服务

bash 复制代码
# 列出所有已安装模型
ollama start

# 运行大模型
ollama run qwq:latest

2、修改env配置文件

注意:有些模型是不支持tool调用的,比如:ollama中下载的deepseek-r1,所以选大模型的时候,要选择支持tool的大模型

ini 复制代码
BASE_URL=http://localhost:11434/v1/
MODEL=qwq:latest
API_KEY=ollama

3、运行client

四、MCP服务端

一)服务端代码

创建服务端文件 server.py

python 复制代码
from mcp.server.fastmcp import FastMCP

# 初始化FastMCP服务器
mcp = FastMCP("filesystem")

@mcp.tool()
async def create_file(file_name: str, content: str) -> str:
    """
    创建文件
    :param file_name: 文件名
    :param content: 文件内容
    """
    with open(file_name, "w", encoding="utf-8") as file:
        file.write(content)
        return "创建成功"

@mcp.tool()
async def read_file(file_name: str) -> str:
    """
    读取文件内容
    :param file_name: 文件名
    """
    with open(file_name, "r", encoding="utf-8") as file:
        return file.read()

@mcp.tool()
async def write_file(file_name: str, content: str) -> str:
    """
    写入文件内容
    :param file_name: 文件名
    :param content: 文件内容
    """
    with open(file_name, "w", encoding="utf-8") as file:
        file.write(content)
        return "写入成功"

if __name__ == "__main__":
    # 以标准 I/O 方式运行 MCP 服务器
    mcp.run(transport="stdio")

二)服务端代码详解

服务端代码的逻辑主要是:

1、初始化服务

启动一个名为 filesystem 的MCP服务

ini 复制代码
mcp = FastMCP("filesystem")

2、创建tool方法

通过@mcp.tool()装饰器注册三个 MCP 服务器工具:read_filewrite_filecreate_file,能够被客户端调用。

注意:方法注释一定要写上,内容包含:tool的功能,入参,出参,他是服务端tool的说明书,是大模型能否成功调用该方法的关键。

python 复制代码
@mcp.tool()
async def read_file(file_name: str) -> str:
    """
    读取文件内容
    :param file_name: 文件名
    """
    with open(file_name, "r", encoding="utf-8") as file:
        return file.read()

@mcp.tool()
async def write_file(file_name: str, content: str) -> str:
    """
    写入文件内容
    :param file_name: 文件名
    :param content: 文件内容
    """
    with open(file_name, "w", encoding="utf-8") as file:
        file.write(content)
        return "写入成功"

@mcp.tool()
async def create_file(file_name: str, content: str) -> str:
    """
    创建文件
    :param file_name: 文件名
    :param content: 文件内容
    """
    with open(file_name, "w", encoding="utf-8") as file:
        file.write(content)
        return "创建成功"

3、服务端入口代码

ini 复制代码
# 以标准 I/O 方式运行 MCP 服务器
mcp.run(transport="stdio")

通过 mcp.run(transport='stdio') 启动 MCP 服务器,采用标准 I/O 通信方式,等待客户 端调用,适合客户端和服务端在同一台电脑上通信。

stdio 模式是一种本地进程间通信(IPC,Inter-Process Communication)的方式,它需要服务端程序作为子进程运行,并通过标准输入输出 ( stdin / stdout )进行数据交换。

因此,当我们指定 transport='stdio' 运行 MCP 服务器时,需要先启动服务端程序server.py ,然后再启动客户端程序 client.py

这样客户端才能和服务端通信。

4、启动服务

我们可以在启动一个命令行运行服务端程序:

arduino 复制代码
uv run server.py

五、修改客户端使其能与服务端通信

一)客户端核心逻辑

前面,我们写了客户端代码,但是只是说接通了在线或本地大模型,客户端若需要与服务端通信,就需要修改客户端代码了,客户端的核心逻辑就要调整为:

1、启动并初始化 MCP 客户端

2、连接MCP服务端

2、列出 MCP 服务器上的工具

3、运行交互式聊天循环,处理用户对话

  • 将用户输入发送给 OpenAI 模型
  • 如果模型想调用 MCP 工具(Function Calling),就执行 call_tool
  • 将结果重新发给模型,并返回最终回答

二)修改原有客户端代码

在原有基础上修改代码,只需要增加或修改一下代码即可:

1、增加连接服务端的方法

python 复制代码
async def connect_to_server(self, server_script_path: str):
        """
        连接到 MCP 服务器
        """
        is_python = server_script_path.endswith('.py')
        is_js = server_script_path.endswith('.js')
        if not (is_python or is_js):
            raise ValueError("不支持的文件类型")

        command = "python" if is_python else "node"
        server_params = StdioServerParameters(command=command,
                                              args=[server_script_path],
                                              env=None)
        # 启动 MCP 服务器并建立通信
        stdio_transport = await self.exit_stack.enter_async_context(
            stdio_client(server_params))
        self.stdio, self.write = stdio_transport
        self.session = await self.exit_stack.enter_async_context(
            ClientSession(self.stdio, self.write))

        await self.session.initialize()

2、增加列出服务端工具列表的方法

python 复制代码
async def list_tools(self):
        """列出所有工具"""
        # 列出 MCP 服务器上的工具
        response = await self.session.list_tools()
        tools = response.tools
        print("已连接到服务器,server支持以下工具:", [tool.name for tool in tools])

3、修改用户对话逻辑

python 复制代码
async def process_query(self, query: str) -> str:
        """
        调用大模型处理用户输入
        """
        messages = [{"role": "user", "content": query}]

        response = await self.session.list_tools()

        available_tools = [{
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "input_schema": tool.inputSchema
            }
        } for tool in response.tools]
        print('服务端工具列表', available_tools)

        response = self.client.chat.completions.create(model=self.model,
                                                       messages=messages,
                                                       tools=available_tools)

        # 处理返回的内容
        content = response.choices[0]
        if content.finish_reason == "tool_calls":
            # 如何发现要使用工具,就执行工具
            tool_call = content.message.tool_calls[0]
            tool_name = tool_call.function.name
            tool_args = json.loads(tool_call.function.arguments)

            # 执行工具
            result = await self.session.call_tool(tool_name, tool_args)
            print(f"\n\n[Calling tool {tool_name} with args {tool_args}]\n\n")

            # 将模型返回的原始消息和工具执行的结果都添加到messages中
            messages.append(content.message.model_dump())
            messages.append({
                "role": "tool",
                "content": result.content[0].text,
                "tool_call_id": tool_call.id,
            })

            # 将上面的结果再返回给大模型生产最终的结果
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
            )
            return response.choices[0].message.content

        return content.message.content

4、入口方法也要做调整

csharp 复制代码
async def main():

    # 启动并初始化 MCP 客户端
    client = MCPClient()
    try:
        # 连接到 MCP 服务器
        await client.connect_to_server('server.py')
        # 列出 MCP 服务器上的工具
        await client.list_tools()
        # 运行交互式聊天循环,处理用户对话
        await client.chat_loop()
    finally:
        # 清理资源
        await client.clean()

5、开头的导入内容也要对应做修改

python 复制代码
import asyncio
import os
from openai import OpenAI
from dotenv import load_dotenv
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import json

6、客户端最终完整代码

python 复制代码
import asyncio
import os
from openai import OpenAI
from dotenv import load_dotenv
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import json

# 加载 .env 文件
load_dotenv()


class MCPClient:

    def __init__(self):
        """初始化 MCP 客户端"""
        self.exit_stack = AsyncExitStack()
        self.api_key = os.getenv("API_KEY")  # 读取 OpenAI API Key
        self.base_url = os.getenv("BASE_URL")  # 读取 BASE URL
        self.model = os.getenv("MODEL")  # 读取 model

        if not self.api_key:
            raise ValueError("未找到 API KEY. 请在.env文件中配置API_KEY")

        self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)

    async def process_query(self, query: str) -> str:
        """
        调用大模型处理用户查询并根据返回的tools列表调用对应工具
        """
        messages = [{"role": "user", "content": query}]

        response = await self.session.list_tools()

        available_tools = [{
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "input_schema": tool.inputSchema
            }
        } for tool in response.tools]
        print('服务端工具列表', available_tools)

        response = self.client.chat.completions.create(model=self.model,
                                                       messages=messages,
                                                       tools=available_tools)

        # 处理返回的内容
        content = response.choices[0]
        if content.finish_reason == "tool_calls":
            # 如何发现要使用工具,就执行工具
            tool_call = content.message.tool_calls[0]
            tool_name = tool_call.function.name
            tool_args = json.loads(tool_call.function.arguments)

            # 执行工具
            result = await self.session.call_tool(tool_name, tool_args)
            print(f"\n\n[Calling tool {tool_name} with args {tool_args}]\n\n")

            # 将模型返回的原始消息和工具执行的结果都添加到messages中
            messages.append(content.message.model_dump())
            messages.append({
                "role": "tool",
                "content": result.content[0].text,
                "tool_call_id": tool_call.id,
            })

            # 将上面的结果再返回给大模型生产最终的结果
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
            )
            return response.choices[0].message.content

        return content.message.content

    async def chat_loop(self):
        """运行交互式聊天循环"""
        print("MCP 客户端已启动!输入 'exit' 退出")

        while True:
            try:
                query = input("问: ").strip()
                if query.lower() == 'exit':
                    break

                response = await self.process_query(query)
                print(f"AI回复: {response}")

            except Exception as e:
                print(f"发生错误: {str(e)}")

    async def clean(self):
        """清理资源"""
        await self.exit_stack.aclose()

    async def connect_to_server(self, server_script_path: str):
        """
        连接到 MCP 服务器
        """
        is_python = server_script_path.endswith('.py')
        is_js = server_script_path.endswith('.js')
        if not (is_python or is_js):
            raise ValueError("不支持的文件类型")

        command = "python" if is_python else "node"
        server_params = StdioServerParameters(command=command,
                                              args=[server_script_path],
                                              env=None)
        # 启动 MCP 服务器并建立通信
        stdio_transport = await self.exit_stack.enter_async_context(
            stdio_client(server_params))
        self.stdio, self.write = stdio_transport
        self.session = await self.exit_stack.enter_async_context(
            ClientSession(self.stdio, self.write))

        await self.session.initialize()

    async def list_tools(self):
        """列出所有工具"""
        # 列出 MCP 服务器上的工具
        response = await self.session.list_tools()
        tools = response.tools
        print("已连接到服务器,server支持以下工具:", [tool.name for tool in tools])


async def main():

    # 启动并初始化 MCP 客户端
    client = MCPClient()
    try:
        # 连接到 MCP 服务器
        await client.connect_to_server('server.py')
        # 列出 MCP 服务器上的工具
        await client.list_tools()
        # 运行交互式聊天循环,处理用户对话
        await client.chat_loop()
    finally:
        # 清理资源
        await client.clean()


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

7、重新启动客户端

注意:在运行客户端之前,要确保服务端已经运行

arduino 复制代码
uv run client.py

启动客户端后,我们让AI通过MCP服务帮我们做一件事:在当前目录下aaa.txt文件内写入一句话:今天天气不错,心情很好!若文件不存在就创建

如下图,客户端启动后,列出了服务端可用的工具列表:['read_file', 'write_file', 'create_file']

然后,AI自动调用了服务端的工具创建了文件,并写入了内容

六、更多MCP服务器合集导航地址

号称最大的MCP集合导航站:mcp.so/

MCP集合:github.com/ahujasid/bl...

官方MCP合集:github.com/modelcontex...

Github热门MCP导航:github.com/punkpeye/aw...

七、结语

以上,我们通过一个极其简单的案例,让MCP客户端与服务端进行通信,并成功调用服务端的对应工具,帮我们完成了一件事情。这意味着,AI可以通过MCP服务完成几乎任何事情,比如:操作电脑本地文件夹,帮我们浏览网页并获取内容存,然后存入本地等等很多场景,想象空间很大,而这一切都是由AI驱动完成的!

MCP的功能远不止此,它还支持SSE传输模式,实现服务器与客户端异地运行;提供Resources类资源接口和Prompt类提示词模板;通过标准化协议促进开发者协作,已有数千服务器可供调用。

快快实战起来吧,何以破局,唯有行动!

相关推荐
用户277844910499313 小时前
借助DeepSeek智能生成测试用例:从提示词到Excel表格的全流程实践
人工智能·python
机器之心13 小时前
刚刚,DeepSeek公布推理时Scaling新论文,R2要来了?
人工智能
算AI15 小时前
人工智能+牙科:临床应用中的几个问题
人工智能·算法
几米哥16 小时前
从思考到行动:AutoGLM沉思如何让AI真正"动"起来
llm·aigc·chatglm (智谱)
凯子坚持 c16 小时前
基于飞桨框架3.0本地DeepSeek-R1蒸馏版部署实战
人工智能·paddlepaddle
你觉得20517 小时前
哈尔滨工业大学DeepSeek公开课:探索大模型原理、技术与应用从GPT到DeepSeek|附视频与讲义下载方法
大数据·人工智能·python·gpt·学习·机器学习·aigc
8K超高清17 小时前
中国8K摄像机:科技赋能文化传承新图景
大数据·人工智能·科技·物联网·智能硬件
hyshhhh17 小时前
【算法岗面试题】深度学习中如何防止过拟合?
网络·人工智能·深度学习·神经网络·算法·计算机视觉
薛定谔的猫-菜鸟程序员17 小时前
零基础玩转深度神经网络大模型:从Hello World到AI炼金术-详解版(含:Conda 全面使用指南)
人工智能·神经网络·dnn