2026最新:MCP开发与使用指南

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
相关推荐
小小疯狐狸15 分钟前
我踩过的Agent harness几个坑:上下文越跑越慢
人工智能
kaixin_啊啊43 分钟前
【零基础学AI】第 30 章——对话记录与结构化输出
人工智能
泯泷1 小时前
让知识"长骨架"——RAPTOR、GraphRAG 与 OpenViking(四)
人工智能·llm
kaixin_啊啊2 小时前
【零基础学AI】第 30 章课后练习与答案
人工智能
电科_银尘2 小时前
【深度学习项目实战·33】自定义数据集:把自己的图片接入PyTorch
人工智能·pytorch·深度学习
武子康2 小时前
单卡 A6000 跑 Qwen-Drive:怎样留下可比较的第一条基线?
linux·人工智能·深度学习
严肃小白兔2 小时前
第 06/24 期:神经网络的反向传播算法:从链式法则到梯度计算
人工智能·神经网络·算法
后端小肥肠3 小时前
公众号漫画又挖到长尾赛道:家常菜美食,流量稳定长期有收益
人工智能·aigc·agent