如何搭建 MCP 服务端与客户端

如何搭建 MCP 服务端与客户端

关键词:MCP、stdio、Streamable HTTP、Python SDK、TypeScript SDK、Cursor

前置:读过《HTTP 与 MCP:不是替代,而是分层》会更容易理解角色划分

环境建议:Python ≥ 3.10(SDK 2.x)或 Node.js ≥ 20

本文按「先起一个最小 Server → 再用 Client / Host 连上 → 再接到 Cursor 」的顺序,给你一条可复现的落地路径。重点用 Python 官方 SDK(生态与文档最全),并附 TypeScript / Host 配置对照。


一、先弄清你在搭什么

text 复制代码
┌──────────────────────────────────────────────┐
│ Host(Cursor / Claude Desktop / 自研 Agent)  │
│   └─ MCP Client(连接器,一对一连一个 Server) │
└────────────────────┬─────────────────────────┘
                     │  JSON-RPC
                     │  stdio 或 Streamable HTTP
┌────────────────────▼─────────────────────────┐
│ MCP Server(你要写的程序)                     │
│   Tools / Resources / Prompts                │
└──────────────────────────────────────────────┘
你要交付的 实际产物
MCP 服务端 一个进程:注册工具,stdio 或 HTTP 监听协议消息
MCP 客户端 拉起/连接 Server,做 initializelist_toolscall_tool
接到 Cursor 多数时候 不用自写 Client :在 mcp.json 里声明如何启动 Server,Cursor 自带 Client

工程上最常见组合:

  1. 只写 Server + 配进 Cursor / Claude Desktop(90% 场景)
  2. 自写 Client(自研 Agent、后端编排、单测)
  3. Server + Client 都写(平台团队、多 Host 适配)

二、环境准备

Python(推荐入门)

bash 复制代码
# Windows PowerShell 示例
uv init mcp-demo
cd mcp-demo
uv venv
.\.venv\Scripts\activate
uv add "mcp[cli]"
  • mcp:SDK 本体
  • [cli]:附带 mcp dev / mcp run / mcp install 等命令(调试很方便)

要求:Python MCP SDK ≥ 2.0(与现行规范对齐)。

TypeScript(可选)

bash 复制代码
npm init -y
npm install @modelcontextprotocol/server @modelcontextprotocol/client zod
npm install -D typescript tsx

包已拆成 @modelcontextprotocol/server@modelcontextprotocol/client(v2 线)。


三、搭建最小 MCP 服务端(Python)

目标:暴露一个工具 add(a, b),用 stdio 运行(本地 Host 最常用)。

3.1 完整 server.py

python 复制代码
"""最小 MCP Server:stdio + 一个加法工具。"""
from __future__ import annotations

import logging

from mcp.server import MCPServer

# stdio 模式下禁止 print 到 stdout,否则会污染 JSON-RPC
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

mcp = MCPServer("demo-calc")


@mcp.tool()
def add(a: int, b: int) -> int:
    """将两个整数相加并返回结果。

    Args:
        a: 第一个加数
        b: 第二个加数
    """
    logger.info("add called: a=%s b=%s", a, b)
    return a + b


@mcp.tool()
async def echo(text: str) -> str:
    """原样返回文本,用于连通性自检。

    Args:
        text: 任意字符串
    """
    return text


if __name__ == "__main__":
    # 本地 Host(Cursor / Claude Desktop)默认用 stdio 拉起本进程
    mcp.run(transport="stdio")

要点:

说明
MCPServer("name") Server 元信息,Host 侧可见
@mcp.tool() 用类型注解 + docstring 生成 tool schema
mcp.run(transport="stdio") 从 stdin 读请求、向 stdout 写响应
日志用 logging 默认打到 stderr,不会破坏协议

3.2 用 Inspector 冒烟(不接大模型)

bash 复制代码
uv run mcp dev server.py
# 或:npx @modelcontextprotocol/inspector

在 Inspector 里应能看到 add / echo,并手动调用验证返回值。

3.3 再做一个「像真业务」的 Tool(包装 HTTP)

MCP Server 内部仍然可以调你们现有 HTTP API:

python 复制代码
import os
from typing import Any

import httpx2
from mcp.server import MCPServer

mcp = MCPServer("order-tools")
BASE = os.environ["ORDER_API_BASE"]  # 例如 https://api.example.com
TOKEN = os.environ.get("ORDER_API_TOKEN", "")


@mcp.tool()
async def get_order_logistics(order_id: str) -> str:
    """根据订单号查询物流轨迹。

    Args:
        order_id: 业务订单号
    """
    headers = {"Authorization": f"Bearer {TOKEN}"} if TOKEN else {}
    async with httpx2.AsyncClient(timeout=30.0) as client:
        r = await client.get(f"{BASE}/api/orders/{order_id}/logistics", headers=headers)
        if r.status_code >= 400:
            return f"查询失败: HTTP {r.status_code} {r.text[:200]}"
        data: dict[str, Any] = r.json()
        # 返回给模型的应是简洁可读文本/结构化摘要,而不是整包 HTML
        return str(data)

这就是前面博客说的模式:HTTP 仍是业务真相源,MCP 是给模型用的薄适配层

3.4 TypeScript 服务端对照(stdio)

ts 复制代码
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio"; // 以当前 SDK 导出名为准
import * as z from "zod";

function createServer(): McpServer {
  const server = new McpServer({ name: "demo-calc", version: "1.0.0" });

  server.registerTool(
    "add",
    {
      description: "将两个整数相加",
      inputSchema: z.object({
        a: z.number().describe("第一个加数"),
        b: z.number().describe("第二个加数"),
      }),
    },
    async ({ a, b }) => ({
      content: [{ type: "text", text: String(a + b) }],
    }),
  );

  return server;
}

// 工厂交给 serveStdio:由 Host 以子进程方式拉起
await serveStdio(createServer);

TS 侧习惯用 Zod 一份 schema:同时生成模型可见的 JSON Schema + 运行时校验。

具体 import 路径以你安装的 SDK 小版本为准;v2 文档入口:ts.sdk.modelcontextprotocol.io


四、搭建 MCP 客户端(Python)

客户端要做的事只有三步:连上 → 握手 → 发现并调用

4.1 方式 A:进程内直连(单测最快)

SDK 2.x 支持 Client(mcp) 直连 Server 对象,不走子进程:

python 复制代码
import asyncio

from mcp import Client

from server import mcp  # 上面的 MCPServer 实例


async def main() -> None:
    async with Client(mcp) as client:
        # 部分版本会在上下文里自动 initialize;以你安装的 SDK 为准
        tools = await client.list_tools()
        print("tools:", [t.name for t in tools.tools] if hasattr(tools, "tools") else tools)

        result = await client.call_tool("add", {"a": 1, "b": 2})
        # v2 常见字段:structured_content / content,按实际返回打印
        print(result)


asyncio.run(main())

适合:CI、本地单元测试、开发期快速回归。

4.2 方式 B:stdio 拉起真实子进程(更接近 Host)

这是官方「自建 Client」教程的经典写法(ClientSession + stdio_client):

python 复制代码
import asyncio
from contextlib import AsyncExitStack

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


async def main() -> None:
    # 用 uv 保证依赖与解释器一致(Windows 也可)
    params = StdioServerParameters(
        command="uv",
        args=["run", "server.py"],
        # cwd="D:/path/to/mcp-demo",  # 如需固定工作目录可打开
        env=None,  # 或传入 {"ORDER_API_TOKEN": "..."}
    )

    async with AsyncExitStack() as stack:
        read, write = await stack.enter_async_context(stdio_client(params))
        session = await stack.enter_async_context(ClientSession(read, write))

        await session.initialize()  # 协议握手:版本与 capabilities

        listed = await session.list_tools()
        print("tools:", [t.name for t in listed.tools])

        result = await session.call_tool("add", {"a": 40, "b": 2})
        # CallToolResult.content 通常是一组 TextContent
        for item in result.content:
            print(getattr(item, "text", item))


asyncio.run(main())

调用链:

text 复制代码
Client ──spawn──▶ python/uv server.py
       ◀─stdio JSON-RPC─▶
initialize → list_tools → call_tool(add)

4.3 方式 C:把 Client 接到大模型(完整 Agent 环)

伪流程(Anthropic / OpenAI / 通义均可,结构相同):

text 复制代码
1. session.list_tools()  → 转成模型的 tools / functions schema
2. 用户提问 → 调 Chat Completions(带 tools)
3. 若模型返回 tool_call → session.call_tool(name, args)
4. 把工具结果再塞回对话 → 模型继续生成最终回答
5. 循环直到模型不再请求工具

官方完整示例见:Build a client。要点是:MCP Client 只管工具通道,LLM SDK 管推理;两者不要揉成一团。

4.4 远程 Server:Streamable HTTP(概念)

本地开发优先 stdio;要给多人共享或部署到集群时,Server 走 Streamable HTTP(单一 MCP 端点,POST 发 JSON-RPC,可选 SSE 流式)。

客户端侧改为连接 URL(带 Bearer/OAuth),而不是 StdioServerParameters。鉴权、网关、TLS 按常规 HTTP 服务治理即可------此时 HTTP 是 传输 binding,上面仍是 MCP 语义。


五、接到 Cursor / Claude Desktop(不写 Client)

5.1 Cursor

配置文件二选一:

范围 路径
项目级 .cursor/mcp.json
全局 ~/.cursor/mcp.json(Windows 多为 %USERPROFILE%\.cursor\mcp.json

示例:

json 复制代码
{
  "mcpServers": {
    "demo-calc": {
      "command": "uv",
      "args": ["run", "server.py"],
      "cwd": "D:/workspace/myspace/mcp-demo",
      "env": {
        "ORDER_API_TOKEN": "不要把密钥写进仓库"
      }
    }
  }
}

操作建议:

  1. 保存后打开 Cursor Settings → Tools & MCP,确认 Server 绿灯
  2. 对话里让模型「用 add 工具算 1+2」,应出现工具调用审批/结果
  3. 项目级配置优先于全局

5.2 Claude Desktop

编辑 claude_desktop_config.json(Windows 一般在 %APPDATA%\Claude\):

json 复制代码
{
  "mcpServers": {
    "demo-calc": {
      "command": "uv",
      "args": [
        "--directory",
        "D:/workspace/myspace/mcp-demo",
        "run",
        "server.py"
      ]
    }
  }
}

改完后完全退出并重启桌面端。


六、推荐目录结构

text 复制代码
mcp-demo/
├── server.py              # MCP Server 入口
├── client_stdio.py        # 自测 Client(可选)
├── pyproject.toml         # uv / 依赖
├── .env.example           # 密钥模板(不提交真值)
├── .cursor/
│   └── mcp.json           # Cursor 项目级接入
└── README.md

多人协作时:mcp.json 里的 cwd / 绝对路径尽量改为相对路径或文档说明,避免每人机器盘符不同。


七、调试清单与常见坑

现象 原因与处理
Host 连上后立即断开 / 协议错乱 Server 往 stdout print 了日志 → 改用 logging(stderr)
Cursor 显示 Server 失败 command 不在 PATH;改用 uv/python 绝对路径,并设 cwd
工具列表为空 @mcp.tool(),或跑错了文件;用 Inspector 先验
调用超时 工具里同步阻塞太久;改为 async + 合理 timeout
密钥泄漏 不要写进仓库;用 env 注入或密钥管理
Windows 路径 JSON 里用 / 或正确转义 \\
SDK API 对不上文档 确认是 Python SDK 2.x ;v1 的 FastMCP / 部分 import 已变更

调试顺序建议:

text 复制代码
Inspector 能调通
  → client_stdio.py 能 list/call
  → 再配 Cursor mcp.json
  → 最后才接业务 HTTP / 鉴权

八、最小验收标准

搭完后,满足下面四条就算「通了」:

  1. mcp dev / Inspector 能列出并调用 add
  2. 自写 client_stdio.pyinitialize + call_tool
  3. Cursor(或 Claude Desktop)配置后工具可见
  4. 对话中模型能发起工具调用并返回正确结果

再往下才是:Resources、Prompts、OAuth、Streamable HTTP 多租户、审计日志------那些属于「生产硬化」,不是第一天的事。


九、小结

步骤 做什么
1 uv add "mcp[cli]",写 MCPServer + @mcp.tool
2 mcp.run(transport="stdio"),日志只走 stderr
3 Inspector / 内存 Client / stdio ClientSession 三选一验证
4 .cursor/mcp.json 或 Claude Desktop 配置,让 Host 自带 Client 接入
5 工具内部再调现有 HTTP/DB,而不是推倒重来

记住分工:Server 暴露「模型可调用的能力」;Client(或 Cursor)负责握手与调用;LLM 负责「何时调用」。三者边界清晰,MCP 项目才不容易长成泥球。


参考

(API 细节以你锁定的 SDK 小版本为准;文中示例面向 Python SDK 2.x 与 stdio 本地接入场景。)