跳转至

MCP 编程入门

模型上下文协议(MCP)是一个开源协议,用标准化方式让大语言模型(LLM)连接各种数据源与工具, 相当于 AI 应用的 USB-C 接口。

本页从零编写 MCP 服务器与客户端:FastMCP 工具 · Inspector 调试 · Sampling 人工监督 · Claude Desktop 配置 · DeepSeek / LangChain / cline 集成

🟢 MCP 速览

Tools · Prompts · Resources · Sampling · Roots · Transports · stdio · SSE

核心功能 说明
Tools 工具 本文重点,服务于通用大语言模型
Prompts 提示词 生成 Prompt 模板
Resources 资源 客户端可选择的预设资源,支持自定义协议
Sampling 采样 在工具执行前后提供接口,可用于人工确认
Roots 根目录
Transports 传输层 stdio(标准输入/输出)与 SSE(服务器发送事件)

大部分功能服务于 Claude 客户端。本文更希望编写的 MCP 服务器服务于通用大语言模型, 因此以「工具」为重点,其余功能放在最后简单讲解。

传输层中 stdio 更为常用,本文以 stdio 为例。环境:Python 3.11,用 uv 管理项目。

graph LR
    LLM[大语言模型<br/>DeepSeek / GPT-4o] <--> C[MCP 客户端]
    C <-->|stdio| S[MCP 服务器]
    S --> T[外部工具 / API<br/>智谱搜索 / FLUX 出图]

🛠️ 开发 MCP 服务器

本节实现一个用于网络搜索的 MCP 服务器。首先用 uv 初始化项目, uv 官方文档见 docs.astral.sh/uv

初始化项目
# 初始化项目
uv init mcp_getting_started
cd mcp_getting_started

# 创建虚拟环境并进入虚拟环境
uv venv
.venv\Scripts\activate.bat

# 安装依赖
uv add "mcp[cli]" httpx openai

激活命令与平台相关

示例中的 .venv\Scripts\activate.bat 是 Windows 路径,Linux/macOS 对应 .venv/bin/activate

MCP 提供了两个对象:mcp.server.FastMCPmcp.server.Server,其中 FastMCP 是更高层的封装, 这里使用它。创建 web_search.py

web_search.py
import httpx
from mcp.server import FastMCP

# 初始化 FastMCP 服务器
app = FastMCP('web-search')

实现工具方法非常简单:用 @app.tool() 装饰器装饰函数即可。函数名作为工具名称,参数作为工具参数, 注释用于描述工具、参数及返回值。

搜索直接使用智谱接口,它不仅能搜索到相关结果链接,还会生成对应链接中文章总结后的内容,且现阶段免费。

资源 链接
Web Search Pro 文档 bigmodel.cn
API Key 生成 用户中心
web_search.py
@app.tool()
async def web_search(query: str) -> str:
    """
    搜索互联网内容

    Args:
        query: 要搜索内容

    Returns:
        搜索结果的总结
    """

    async with httpx.AsyncClient() as client:
        response = await client.post(
            'https://open.bigmodel.cn/api/paas/v4/tools',
            headers={'Authorization': '换成你自己的API KEY'},  # (1)!
            json={
                'tool': 'web-search-pro',
                'messages': [
                    {'role': 'user', 'content': query}
                ],
                'stream': False
            }
        )

        res_data = []
        for choice in response.json()['choices']:  # (2)!
            for message in choice['message']['tool_calls']:
                search_results = message.get('search_result')
                if not search_results:
                    continue
                for result in search_results:
                    res_data.append(result['content'])

        return '\n\n\n'.join(res_data)
  1. 换成自己在用户中心申请的 API Key。
  2. 逐层解析 choices → tool_calls → search_result,拼接所有 content

添加运行服务器的代码:

web_search.py
if __name__ == "__main__":
    app.run(transport='stdio')

🔍 调试 MCP 服务器

使用官方提供的 Inspector 可视化工具调试服务器(需先安装 Node 环境)。

mcp dev web_search.py
npx -y @modelcontextprotocol/inspector uv run web_search.py

运行成功后打开提示的地址,点击左侧的 Connect 按钮连接服务,然后切换到 Tools 栏, 点击 List Tools 即可看到刚写的工具并开始调试。

  • 启动 Inspector
  • 点击 Connect 连接服务
  • 切换到 ToolsList Tools 调试

🧩 开发 MCP 客户端

直连调用工具

先看如何在客户端调用刚才开发的 MCP 服务器中的工具:

client.py
import asyncio

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

# 为 stdio 连接创建服务器参数
server_params = StdioServerParameters(
    # 服务器执行的命令,这里使用 uv 来运行 web_search.py
    command='uv',
    # 运行的参数
    args=['run', 'web_search.py'],
    # 环境变量,默认为 None,表示使用当前环境变量
    # env=None
)


async def main():
    # 创建 stdio 客户端
    async with stdio_client(server_params) as (stdio, write):
        # 创建 ClientSession 对象
        async with ClientSession(stdio, write) as session:
            # 初始化 ClientSession
            await session.initialize()

            # 列出可用的工具
            response = await session.list_tools()
            print(response)

            # 调用工具
            response = await session.call_tool('web_search', {'query': '今天杭州天气'})
            print(response)


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

由于 Python 脚本需要在虚拟环境中运行,这里通过 uv 启动脚本。

让 DeepSeek 调用 MCP 工具

dotenv 管理相关环境变量:

.env
OPENAI_API_KEY=sk-89baxxxxxxxxxxxxxxxxxx
OPENAI_BASE_URL=https://api.deepseek.com
OPENAI_MODEL=deepseek-chat

Key 已脱敏

上面的 OPENAI_API_KEY 是示例值,使用时请换成自己的 Key。

首先编写 MCPClient 类:

client.py
import json
import asyncio
import os
from typing import Optional
from contextlib import AsyncExitStack

from openai import OpenAI
from dotenv import load_dotenv

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


load_dotenv()


class MCPClient:
    def __init__(self):
        self.session: Optional[ClientSession] = None
        self.exit_stack = AsyncExitStack()
        self.client = OpenAI()

添加 connect_to_server 方法初始化 MCP 服务器的 session:

client.py
    async def connect_to_server(self):
        server_params = StdioServerParameters(
            command='uv',
            args=['run', 'web_search.py'],
            env=None
        )

        stdio_transport = await self.exit_stack.enter_async_context(
            stdio_client(server_params))
        stdio, write = stdio_transport
        self.session = await self.exit_stack.enter_async_context(
            ClientSession(stdio, write))

        await self.session.initialize()

再实现一个用于调用 MCP 服务器、处理与 DeepSeek 交互的方法:

client.py
    async def process_query(self, query: str) -> str:
        # 这里需要通过 system prompt 约束大语言模型,
        # 否则会出现不调用工具、自己乱回答的情况
        system_prompt = (
            "You are a helpful assistant."
            "You have the function of online search. "
            "Please MUST call web_search tool to search the Internet content before answering."
            "Please do not lose the user's question information when searching,"
            "and try to maintain the completeness of the question content as much as possible."
            "When there is a date related question in the user's question,"
            "please use the search function directly to search and PROHIBIT inserting specific time."
        )

        messages = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": query}
        ]

        # 获取所有 mcp 服务器工具列表信息
        response = await self.session.list_tools()
        # 生成 function call 的描述信息
        available_tools = [{
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "input_schema": tool.inputSchema
            }
        } for tool in response.tools]

        # 请求 deepseek,function call 的描述信息通过 tools 参数传入
        response = self.client.chat.completions.create(
            model=os.getenv("OPENAI_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")

            # 将 deepseek 返回的调用工具数据和工具执行完成后的数据都存入 messages 中
            messages.append(content.message.model_dump())
            messages.append({
                "role": "tool",
                "content": result.content[0].text,
                "tool_call_id": tool_call.id,
            })

            # 将上面的结果再返回给 deepseek,用于生成最终结果
            response = self.client.chat.completions.create(
                model=os.getenv("OPENAI_MODEL"),
                messages=messages,
            )
            return response.choices[0].message.content

        return content.message.content

接着实现循环提问,以及在最后退出后关闭 session:

client.py
    async def chat_loop(self):
        while True:
            try:
                query = input("\nQuery: ").strip()

                if query.lower() == 'quit':
                    break

                response = await self.process_query(query)
                print("\n" + response)

            except Exception as e:
                import traceback
                traceback.print_exc()

    async def cleanup(self):
        """Clean up resources"""
        await self.exit_stack.aclose()

最后完成运行客户端的代码:

client.py
async def main():
    client = MCPClient()
    try:
        await client.connect_to_server()
        await client.chat_loop()
    finally:
        await client.cleanup()


if __name__ == "__main__":
    import sys

    asyncio.run(main())

连接多个 MCP 服务器

这是最精简的代码,没有实现记录上下文消息等功能,目的只是用最简单的代码了解如何通过大语言模型调动 MCP 服务器。这里只演示连接单个服务器;若需连接多个,只需循环 connect_to_server 中的代码, 将其封装成类,把所有服务器中的工具遍历生成一个大的 available_tools,再根据大语言模型的返回 结果调用即可。


✋ Sampling:工具执行前后的人工监督

MCP 提供的 Sampling 功能,本质是在工具执行前后提供一个接口,可以在工具执行前后执行一些操作。 例如调用本地文件删除工具时,通常希望用户确认后再删除,此时即可使用该功能。

下面实现一个人工监督的小功能。首先创建模拟删除文件的 MCP 服务器:

file_server.py
# 服务端
from mcp.server import FastMCP
from mcp.types import SamplingMessage, TextContent

app = FastMCP('file_server')


@app.tool()
async def delete_file(file_path: str):
    # 创建 SamplingMessage 用于触发 sampling callback 函数
    result = await app.get_context().session.create_message(
        messages=[
            SamplingMessage(
                role='user', content=TextContent(
                    type='text', text=f'是否要删除文件: {file_path} (Y)')
            )
        ],
        max_tokens=100
    )

    # 获取 sampling callback 函数的返回值,并根据返回值进行处理
    if result.content.text == 'Y':
        return f'文件 {file_path} 已被删除!!'


if __name__ == '__main__':
    app.run(transport='stdio')

这里最重要的是通过 create_message 方法创建一个 SamplingMessage 类型的 message, 它会将该 message 发送给 sampling callback 对应的函数。

接着创建客户端代码:

client.py
# 客户端
import asyncio

from mcp.client.stdio import stdio_client
from mcp import ClientSession, StdioServerParameters
from mcp.shared.context import RequestContext
from mcp.types import (
    TextContent,
    CreateMessageRequestParams,
    CreateMessageResult,
)

server_params = StdioServerParameters(
    command='uv',
    args=['run', 'file_server.py'],
)


async def sampling_callback(
        context: RequestContext[ClientSession, None],
        params: CreateMessageRequestParams,
):
    # 获取工具发送的消息并显示给用户
    input_message = input(params.messages[0].content.text)
    # 将用户输入发送回工具
    return CreateMessageResult(
        role='user',
        content=TextContent(
            type='text',
            text=input_message.strip().upper() or 'Y'
        ),
        model='user-input',
        stopReason='endTurn'
    )


async def main():
    async with stdio_client(server_params) as (stdio, write):
        async with ClientSession(
                stdio, write,
                # 设置 sampling_callback 对应的方法
                sampling_callback=sampling_callback
        ) as session:
            await session.initialize()
            res = await session.call_tool(
                'delete_file',
                {'file_path': 'C:/xxx.txt'}
            )
            # 获取工具最后执行完的返回结果
            print(res)


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

stdio 下看不到工具内的 print

目前在工具里面打印的内容使用 stdio_client 无法显示到命令行窗口。调试时可以使用 mcp.shared.memory.create_connected_server_and_client_session

debug.py
# 客户端
from mcp.shared.memory import (
    create_connected_server_and_client_session as create_session
)
# 这里需要引入服务端的 app 对象
from file_server import app


async def sampling_callback(context, params):
    ...


async def main():
    async with create_session(
        app._mcp_server,
        sampling_callback=sampling_callback
    ) as client_session:
        await client_session.call_tool(
            'delete_file',
            {'file_path': 'C:/xxx.txt'}
        )

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

🖥️ Claude Desktop 加载 MCP Server

打开配置:点击 Developer 菜单,再点击 Edit Config 按钮, 打开 Claude 桌面端的配置文件 claude_desktop_config.json

添加服务器,服务器需放在 mcpServers 层级下,参数有 commandargsenv, 与 StdioServerParameters 初始化时的参数一致。

claude_desktop_config.json
{
  "mcpServers": {
    "web-search-server": {
      "command": "uv",
      "args": [
        "--directory",
        "D:/projects/mcp_getting_started",
        "run",
        "web_search.py"
      ]
    }
  }
}

保存文件后重启 Claude 桌面端即可看到插件。也可以直接在插件目录下运行以下命令安装:

mcp install web_search.py

📦 其他功能:Prompt 与 Resource

MCP 提供了生成 Prompt 模板的功能,使用 prompt 装饰器即可:

prompt_and_resources.py
from mcp.server import FastMCP

app = FastMCP('prompt_and_resources')


@app.prompt('翻译专家')
async def translate_expert(
        target_language: str = 'Chinese',
) -> str:
    return f'你是一个翻译专家,擅长将任何语言翻译成{target_language}。请翻译以下内容:'


if __name__ == '__main__':
    app.run(transport='stdio')

用配置 Claude 桌面端 MCP 服务器的方法添加该服务器后,点击右下角的图标即可使用。 它会提示设置传入的参数,并在聊天窗口生成一个附件。

可以在 Claude 客户端上选择为用户提供的预设资源,同时也支持自定义协议:

prompt_and_resources.py
from mcp.server import FastMCP

app = FastMCP('prompt_and_resources')


@app.resource('echo://static')
async def echo_resource():
    # 返回的是,当用户使用这个资源时,资源的内容
    return 'Echo!'


@app.resource('greeting://{name}')
async def get_greeting(name):
    return f'Hello, {name}!'


if __name__ == '__main__':
    app.run(transport='stdio')

通配符路径暂不支持

目前 Claude 桌面端无法读取资源装饰器设置的 greeting://{name} 这类通配符路径,未来将支持; 但在客户端代码中可以当作资源模板使用。

client.py
import asyncio
from pydantic import AnyUrl

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

server_params = StdioServerParameters(
    command='uv',
    args=['run', 'prompt_and_resources.py'],
)


async def main():
    async with stdio_client(server_params) as (stdio, write):
        async with ClientSession(stdio, write) as session:
            await session.initialize()

            # 获取无通配符的资源列表
            res = await session.list_resources()
            print(res)

            # 获取有通配符的资源列表(资源模板)
            res = await session.list_resource_templates()
            print(res)

            # 读取资源,会匹配通配符
            res = await session.read_resource(AnyUrl('greeting://liming'))
            print(res)

            # 获取 Prompt 模板列表
            res = await session.list_prompts()
            print(res)

            # 使用 Prompt 模板
            res = await session.get_prompt(
                '翻译专家', arguments={'target_language': '英语'})
            print(res)


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

🦜 在 LangChain 中使用 MCP 服务器

langchain-mcp-adapters 可以很方便地将 MCP 服务器集成到 LangChain 中:

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

from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent

from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")

server_params = StdioServerParameters(
    command='uv',
    args=['run', 'web_search.py'],
)

async with stdio_client(server_params) as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()

        # 获取工具列表
        tools = await load_mcp_tools(session)

        # 创建并使用 ReAct agent
        agent = create_react_agent(model, tools)
        agent_response = await agent.ainvoke({'messages': '杭州今天天气怎么样?'})

更详细的使用方法参考 langchain-mcp-adapters


🎨 实战:DeepSeek + cline + 自定义 MCP = 图文大师

使用 VS Code 的 cline 插件,通过 DeepSeek 和自定义的图片生成 MCP 服务器构建一个图文大师应用。

图片生成使用 HuggingFace 上的 FLUX.1-schnell 模型 (模型空间)。 不使用 gradio_client 库,而是用 httpx 手搓,因为 gradio_client 可能出现编码错误的 bug。

image_server.py
# image_server.py

import json
import httpx
from mcp.server import FastMCP


app = FastMCP('image_server')


@app.tool()
async def image_generation(image_prompt: str):
    """
    生成图片
    :param image_prompt: 图片描述,需要是英文
    :return: 图片保存到的本地路径
    """
    async with httpx.AsyncClient() as client:
        data = {'data': [image_prompt, 0, True, 512, 512, 3]}

        # 创建生成图片任务
        response1 = await client.post(
            'https://black-forest-labs-flux-1-schnell.hf.space/call/infer',
            json=data,
            headers={"Content-Type": "application/json"}
        )

        # 解析响应获取事件 ID
        response_data = response1.json()
        event_id = response_data.get('event_id')

        if not event_id:
            return '无法获取事件 ID'

        # 通过流式的方式拿到返回数据
        url = f'https://black-forest-labs-flux-1-schnell.hf.space/call/infer/{event_id}'
        full_response = ''
        async with client.stream('GET', url) as response2:
            async for chunk in response2.aiter_text():
                full_response += chunk

        return json.loads(full_response.split('data: ')[-1])[0]['url']


if __name__ == '__main__':
    app.run(transport='stdio')

在虚拟环境下使用以下命令打开 MCP Inspector 调试工具:

mcp dev image_server.py

接着在 VS Code 中安装 cline 插件,安装完成后配置 DeepSeek 的 API Key。点击右上角的 MCP Server 按钮打开 MCP Server 列表,切换到 Installed 标签,点击 Configure MCP Servers 编辑自定义 MCP 服务器:

cline MCP 配置
{
  "mcpServers": {
    "image_server": {
      "command": "uv",
      "args": [
        "--directory",
        "D:/projects/mcp_getting_started",
        "run",
        "image_server.py"
      ],
      "env": {},
      "disabled": false,
      "autoApprove": []
    }
  }
}

保存后,服务器连接成功时状态点会显示为绿色,此时即可开始使用:在输入框中输入要写的文章内容, cline 会自动调用工具生成图片并最终输出文章。

相关代码仓库:MCP-Chinese-Getting-Started-Guide