MCP 开发与使用指南
本文解决两个问题:MCP 是什么、怎么以连接 MySQL 为例从零开发一个可接入 WorkBuddy 的 MCP Server。
适用对象:想在 WorkBuddy 等 AI 编程工具中扩展工具能力的开发者。
读完本文,你能独立完成一个可运行 MCP Server 的开发、调试与注册,并能开发连接数据库的只读探查 Server。
1. MCP 是什么
1.1 一句话定义
MCP(Model Context Protocol,模型上下文协议)是 Anthropic 于 2024 年 11 月开源的协议。它规范了 AI 应用与外部工具、数据源之间的通信方式。
1.2 解决的问题
没有 MCP 时,每个 AI 应用接入每个数据源都要单独写适配代码。M 个应用对接 N 个数据源,需要 M×N 套适配。
引入 MCP 后,数据源只需实现一次 MCP Server,所有支持 MCP 的 AI 应用都能直接使用。适配工作量从 M×N 降为 M+N。这类似 USB 接口:外设按统一接口制造,主机按统一接口读取。
1.3 三个角色
| 角色 | 职责 | 例子 |
|---|---|---|
| Host(宿主) | 运行 AI 应用、管理连接、执行安全策略 | WorkBuddy、Claude Desktop、Cursor |
| Client(客户端) | Host 内部的连接器,与一个 Server 保持 1:1 会话 | 在WorkBuddy上配置MCP客户端,无需单独安装 |
| Server(服务端) | 暴露工具、资源、提示模板的独立进程 | 文件系统 Server、数据库 Server、自研业务 Server |
1.4 传输方式
| 传输方式 | 通信载体 | 适用场景 |
|---|---|---|
| stdio | 标准输入输出 | 本地进程,最常用,配置最简单 |
| Streamable HTTP | HTTP POST + 可选 SSE 流 | 远程部署、多客户端共享 |
本指南的示例采用 stdio 传输。
1.5 Server 提供的三类能力
| 能力 | 控制方 | 作用 | 例子 |
|---|---|---|---|
| Tools(工具) | 模型决定调用 | 让模型执行动作、改变状态 | 查询订单、执行数据质量检查、发送消息 |
| Resources(资源) | 应用决定读取 | 向模型注入只读上下文 | 表结构文档、配置文件、指标口径 |
| Prompts(提示模板) | 用户主动选择 | 预置可复用的提示词 | 「生成巡检报告」「代码审查」模板 |
三类能力的设计原则:有副作用或需要计算的操作做成 Tool;只读数据做成 Resource;固定的提问套路做成 Prompt。
2. 开发 MCP Server
本章以连接 MySQL 为例,从零开发一个可运行的 MCP Server,Server 名称 mysql-explorer。
定位为只读探查:AI 能看表、看结构、跑查询,不能改数据。
它暴露 3 个 Tool、2 个 Resource、1 个 Prompt,三类能力各就其位:Tool 执行动作(列清单、看结构、跑查询),Resource 提供只读上下文(表清单、列结构),Prompt 预置探查报告的固定套路。
2.1 环境准备
需要 Python 3.10 以上版本和一个可连接的 MySQL 5.7 / 8.0 实例。执行安装:
bash
pip install "mcp[cli]" pymysql
预期结果:安装 mcp 与 pymysql 及其依赖,无报错退出。
2.2 完整代码
新建文件 mysql_server.py,内容如下。代码段的作用全部写在注释里,读完注释即读懂全部代码:
python
import json
import os
import re
import pymysql
from mcp.server.fastmcp import FastMCP
# 创建 Server 实例;名称会展示给客户端
mcp = FastMCP("mysql-explorer")
# 连接配置:从环境变量读取连接五要素;密钥不写进代码与 args 明文
def _db_conf() -> dict:
return {
"host": os.environ.get("MYSQL_HOST", "127.0.0.1"),
"port": int(os.environ.get("MYSQL_PORT", "3306")),
"user": os.environ.get("MYSQL_USER", "root"),
"password": os.environ.get("MYSQL_PASSWORD", ""),
"database": os.environ.get("MYSQL_DB", ""),
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
"connect_timeout": 5,
}
# 每次调用独立建连、用完即关;DictCursor 让结果直接是字典,模型可读性好
def _fetch_all(sql: str, params: tuple | None = None) -> list[dict]:
"""执行一条 SQL 并取回全部结果行。"""
conn = pymysql.connect(**_db_conf())
try:
with conn.cursor() as cur:
cur.execute(sql, params)
return cur.fetchall()
finally:
conn.close()
# 只读防线
# _assert_read_only 先去注释再取首关键字比对白名单;
# 去注释是为了防 -- DELETE 这类注释伪装绕过
_ALLOWED_HEAD = {"select", "show", "describe", "desc", "explain", "with"}
_COMMENT = re.compile(r"--.*?$|/\*.*?\*/", re.MULTILINE | re.DOTALL)
_TABLE_NAME = re.compile(r"^[A-Za-z0-9_$]+$")
def _assert_read_only(sql: str) -> str:
"""去除注释后校验语句类型为只读,返回可执行语句。非只读直接抛错。"""
cleaned = _COMMENT.sub("", sql).strip().rstrip(";")
head = cleaned.split(None, 1)[0].lower() if cleaned else ""
if head not in _ALLOWED_HEAD:
# 主动抛异常:MCP 会把异常转为错误结果返回给模型,模型据此调整后续动作
raise ValueError(
f"本 Server 只允许只读查询(SELECT / SHOW / DESC / EXPLAIN / WITH),收到:{head or '空语句'}"
)
return cleaned
# 表名白名单:拼接 SHOW FULL COLUMNS 前先校验,杜绝注入
# 内部实现:Tool 与 Resource 共用
def _list_tables_data() -> dict:
rows = _fetch_all("SHOW TABLE STATUS")
return {
"database": _db_conf()["database"],
"tables": [
{"table": r.get("Name"), "rows": r.get("Rows"), "comment": r.get("Comment") or ""}
for r in rows
],
}
def _describe_table_data(table: str) -> dict:
if not _TABLE_NAME.fullmatch(table):
raise ValueError("表名只允许字母、数字、下划线与 $,防止注入")
rows = _fetch_all(f"SHOW FULL COLUMNS FROM `{table}`")
columns = [
{
"column": r.get("Field"),
"type": r.get("Type"),
"nullable": r.get("Null"),
"key": r.get("Key"),
"default": r.get("Default"),
"comment": r.get("Comment") or "",
}
for r in rows
]
return {"table": table, "columns": columns}
# Tool:模型决定调用
# @mcp.tool() 把函数注册为 Tool;类型注解自动生成参数 schema;
# docstring 是模型判断何时调用的依据,必须写清
@mcp.tool()
def list_tables() -> dict:
"""列出当前连接库的全部表,含行数估计与表注释。用于探查前先了解库内有哪些表。"""
return _list_tables_data()
@mcp.tool()
def describe_table(table: str) -> dict:
"""查看指定表的列结构:列名、类型、是否可空、键、注释。
Args:
table: 表名,如 dwd_order
"""
return _describe_table_data(table)
@mcp.tool()
def run_query(sql: str, max_rows: int = 50) -> dict:
"""执行只读 SQL 并返回结果集。只允许 SELECT / SHOW / DESC / EXPLAIN / WITH;
不带 LIMIT 的 SELECT 会被自动包一层 LIMIT,防止大结果集拖垮内存。
Args:
sql: 只读 SQL 语句,如 SELECT COUNT(*) FROM dwd_order
max_rows: 最多返回行数,默认 50
"""
cleaned = _assert_read_only(sql)
# 无 LIMIT 的 SELECT 包一层子查询追加 LIMIT,防大结果集拖垮内存
if cleaned.lower().startswith(("select", "with")) and not re.search(
r"\blimit\s+\d+", cleaned, re.IGNORECASE
):
cleaned = f"SELECT * FROM ({cleaned}) AS _sub LIMIT {int(max_rows)}"
rows = _fetch_all(cleaned)
return {"row_count": len(rows), "rows": rows[: int(max_rows)]}
# Resource:应用决定读取,只读上下文
# URI 模板中大括号为路径参数,客户端按 URI 拉取只读内容
@mcp.resource("mysql://tables")
def resource_tables() -> str:
"""当前库的表清单"""
return json.dumps(_list_tables_data(), ensure_ascii=False, indent=2)
@mcp.resource("mysql://tables/{table}/schema")
def resource_schema(table: str) -> str:
"""指定表的列结构"""
return json.dumps(_describe_table_data(table), ensure_ascii=False, indent=2)
# Prompt:用户主动选用,返回值作为提示词注入会话
@mcp.prompt()
def probe_report(table: str) -> str:
"""生成指定表的数据探查报告提示词"""
return (
f"请针对表 {table} 完成数据探查:先用 describe_table 看列结构,"
f"再用 run_query 统计总行数与关键列的空值情况,"
f"最后输出结论型探查报告。"
)
if __name__ == "__main__":
# 启动 stdio 主循环;Host 通过标准输入输出与本进程通信
mcp.run()
2.3 安全设计:四道防线
| 防线 | 实现位置 | 拦截内容 |
|---|---|---|
| SQL 白名单 | _assert_read_only | INSERT / UPDATE / DELETE / DROP / ALTER 等写语句 |
| 表名校验 | _describe_table_data | 表名中的引号、空格等注入字符 |
| 自动 LIMIT | run_query | 无 LIMIT 的大结果集拖垮内存 |
| 密钥隔离 | mcp.json 的 env 字段 | 密钥出现在 args 与代码明文中 |
先剥注释再取首关键字,是为了防 -- DELETE 这类注释伪装绕过白名单。配套数据库侧防线:为 MCP 单建只读账号。执行:
sql
CREATE USER 'mcp_ro'@'%' IDENTIFIED BY '强密码';
GRANT SELECT ON 你的库名.* TO 'mcp_ro'@'%';
FLUSH PRIVILEGES;
双保险含义:代码白名单拦住 AI 的误操作,数据库权限兜底拦住一切漏网写操作。账号权限与代码校验缺一不可。
2.4 注册到客户端
WorkBuddy 注册 MCP Server 有两种方式:界面配置与配置文件配置。两者效果一致,界面配置的本质也是写入配置文件,任选其一。
方式一,配置文件配置。编辑配置文件 ~/.workbuddy/mcp.json,添加 mcpServers 配置:
json
{
"mcpServers": {
"mysql-explorer": {
"command": "python3",
"args": ["/Users/wenhuadong/temp/mysql_server.py"],
"env": {
"MYSQL_HOST": "127.0.0.1",
"MYSQL_PORT": "3306",
"MYSQL_USER": "mcp_ro",
"MYSQL_PASSWORD": "你的密码",
"MYSQL_DB": "你的库名"
}
}
}
}
方式二,界面配置。在 WorkBuddy 的 「专家-技能-连接器」- 右上角「连接器」 - 自定义连接器 - 添加MCP 
字段含义:
| 字段 | 含义 |
|---|---|
| mcpServers | 所有 Server 的根节点,键名为 Server 的自定义名称 |
| command | 启动命令,stdio 传输下为一个可执行程序 |
| args | 启动参数数组 |
| env | 注入环境变量,连接 MySQL 的五要素全部经此处传入 |
⚠️ 注意:args 必须写绝对路径。相对路径在 WorkBuddy 的工作目录下解析,结果不可控。
💡 提示:本地 stdio 型 Server 注册就以上两种方式。若 Server 以 HTTP 远程部署,注册时改用 url 字段指向服务地址,不再写 command 与 args。
💡 提示:stdio 型 Server 由 WorkBuddy 在会话启动时拉起进程。改完配置必须重开会话,旧会话不生效。
2.5 验证步骤
① 重开 WorkBuddy 会话,让 AI 列出当前可用的 MCP 工具,确认列表中有 mysql-explorer 的三个工具。
② 对 AI 说:列出当前库的表。预期结果:AI 调用 list_tables,返回表名、行数估计与注释。
③ 对 AI 说:看一下 dwd_order 的表结构。预期结果:AI 调用 describe_table,返回列清单。
④ 对 AI 说:统计 dwd_order 有多少行。预期结果:AI 调用 run_query,返回 row_count 与 COUNT(*) 结果。
⑤ 对 AI 说:DELETE FROM dwd_order。预期结果:run_query 抛出错误,提示只允许只读查询,数据不被改动。
2.6 常见故障
第 3 章的通用故障之外,本 Server 另有三类 MySQL 特有故障:
| 现象 | 原因 | 处理 |
|---|---|---|
| 连接失败 Access denied | 用户名或密码错误,或账号无该库权限 | 用同一账号在命令行手动连一次,确认可达 |
| 返回结果被截断 | max_rows 默认 50 | 显式传更大的 max_rows,或在 SQL 里先聚合再取数 |
| 中文乱码 | 连接字符集非 utf8mb4 | _db_conf 中 charset 已设 utf8mb4,检查库表自身字符集 |
2.7 推广到其他服务
MCP 是通用协议,任何能被代码调用的服务都可照本案例开放 MCP 接口,不止数据源。
换数据源:替换 _db_conf 与 _fetch_all,保留四道防线结构,即得 PostgreSQL、Doris 等数据源的探查 Server。
换服务类型:把内部系统能力包装成 Tool,如订单查询、消息发送、数据质量检查、知识库检索;WorkBuddy内置了多种服务MCP。 
3. 测试与调试
3.1 MCP Inspector
官方提供调试工具 Inspector。执行:
bash
mcp dev mysql_server.py
浏览器自动打开调试界面。功能:
| 面板 | 用途 |
|---|---|
| Tools | 列出全部工具,填参数直接调用,查看返回与异常 |
| Resources | 列出资源,点击读取内容 |
| Prompts | 渲染提示模板 |
验收标准:Inspector 中三类能力全部列出且调用成功,再接入客户端。
3.2 常见故障
| 现象 | 原因 | 处理 |
|---|---|---|
| 客户端显示 Server 连接失败 | 脚本路径错误或 Python 版本过低 | 命令行手动运行脚本,确认无报错退出方式是否为等待输入 |
| 工具列表为空 | 未重开会话 | 重开客户端会话 |
| 模型不调用工具 | docstring 缺失或含糊 | 补写 docstring,写清用途与参数含义 |
| 参数校验失败 | 类型注解缺失 | 为所有参数补类型注解,FastMCP 依赖注解生成 schema |
⚠️ 注意:排查 stdio Server 时,禁止在代码中用 print 输出调试信息。stdout 是协议通道,额外输出会破坏协议帧。调试信息写 stderr 或日志文件。
4. 发布形态与安全
4.1 stdio 与 HTTP 的选择
| 判断条件 | 选择 |
|---|---|
| 仅本机一个客户端使用 | stdio |
| 团队共享、多客户端、需集中鉴权 | Streamable HTTP |
HTTP 形态下,把 mcp.run() 改为 mcp.run(transport="streamable-http"),用 uvicorn 等服务托管,客户端配置改用 url 字段指向服务地址。
4.2 安全红线
① Tool 不执行未经校验的 SQL 拼接、文件删除、命令执行。 ② 密钥用 env 字段注入,不写进配置文件的明文 args。 ③ Resource 只暴露只读数据,不做写操作入口。 ④ Host 侧对高危 Tool 开启人工确认(客户端普遍支持工具级确认开关)。
5. 交付前自查清单
- 三类能力(Tool / Resource / Prompt)按第 1.5 节原则归位,未混用
- 每个 Tool 的 docstring 写清用途与参数含义
- 所有参数有类型注解
- Inspector 中三类能力全部调用成功
- 客户端配置中路径为绝对路径
- 密钥经 env 注入,未出现在 args 与代码中
- 排障时未向 stdout 打印调试信息
涉及数据库的 Server(第 2 章)追加自查:
- SQL 白名单与表名校验生效,写语句被拒
- 无 LIMIT 的 SELECT 被自动追加 LIMIT
- 数据库侧已建只读账号并只授予 SELECT