如何搭建 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,做 initialize → list_tools → call_tool |
| 接到 Cursor | 多数时候 不用自写 Client :在 mcp.json 里声明如何启动 Server,Cursor 自带 Client |
工程上最常见组合:
- 只写 Server + 配进 Cursor / Claude Desktop(90% 场景)
- 自写 Client(自研 Agent、后端编排、单测)
- 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": "不要把密钥写进仓库"
}
}
}
}
操作建议:
- 保存后打开 Cursor Settings → Tools & MCP,确认 Server 绿灯
- 对话里让模型「用 add 工具算 1+2」,应出现工具调用审批/结果
- 项目级配置优先于全局
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 / 鉴权
八、最小验收标准
搭完后,满足下面四条就算「通了」:
mcp dev/ Inspector 能列出并调用add- 自写
client_stdio.py能initialize+call_tool - Cursor(或 Claude Desktop)配置后工具可见
- 对话中模型能发起工具调用并返回正确结果
再往下才是: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 本地接入场景。)