资讯动态

OpenClaw技能开发实战:基于MCP协议实现MySQL增删改查

发布时间:2026/9/25 8:19:40 来源:尧图企业网站定制
简介面向OpenClaw技能开发者的自定义实战资源围绕在OpenClaw框架中对MySQL数据库进行增删改查操作这一常见需求提供了一套可直接落地的技能实现方案。压缩包内共3个文件Python脚本使用pymysql等库封装了数据库连接配置与增删改查函数并对异常捕获、参数化查询等做了细致处理Markdown技能文件明确了技能的触发方式、输入参数与返回结果方便框架正确调用HTML页面则提供了一个简单的操作演示入口便于本地快速验证。整体压缩包仅3KB体量轻巧已有164人学习或下载。虽然包体精简但完整呈现了从数据库连接、CRUD函数封装到安全加固的技能构建路径覆盖边界条件测试、SQL注入防范及代码注释规范等工程实践适合初入OpenClaw的开发者快速上手也可供进阶用户参考对照。1. OpenClaw 技能到底怎么落地先把 MySQL 增删改查跑通如果你接触过 OpenClaw应该知道它是一个能把大模型接进真实系统操作的 agent 框架。但框架本身不干活真正干活的是「技能」——也就是一套可复用的工具集。我这篇要拆的是其中一个很典型的技能让 OpenClaw 通过自然语言或人工触发对 MySQL 做增删改查CRUD。它能解决的核心问题有两个一是把业务人员反复找 DBA 要数据的流程自动化二是让 agent 在执行数据库操作时有明确的边界不瞎改数据。适合看这篇的人是我这种数据库运维出身、又想做 LLM 工程化的从业者。你不需要懂深度的机器学习原理但得会 MySQL 基础命令最好碰过一点 agent 工具开发。下面这套方案我在本地环境完整跑通过包括踩坑记录照着做基本能复现。先说结论OpenClaw 的技能本质上是「参数化工具 说明文档 权限控制」的组合理解了这一层写什么技能的思路都一样。2. 技能包的文件结构与配置先把 MCP 和数据源接上2.1 OpenClaw 技能的文件组织方式我最早犯的错是把「技能」理解成一段独立的 Python 脚本。实际上 OpenClaw 里的技能是一个目录里面至少包含三样东西技能描述文件、工具实现文件、以及一份可选的配置模板。描述文件负责告诉 agent「这个技能是干嘛的、什么时候该调用」工具实现文件才是真正干活的代码。一个典型的技能目录长这样openclaw-skills/ └── mysql-crud/ ├── SKILL.md ├── mcp-config.json ├── index.ts ├── package.json └── .env.exampleSKILL.md 写技能的用途声明mcp-config.json 负责把技能挂到 OpenClaw 的 MCP 通道上index.ts 是工具实现。.env.example 存放所有需要注入的敏感参数比如数据库账号密码这样技能代码里不会出现明文凭据。在 OpenClaw 里配置技能时直接指向这个目录即可不用手动拷贝文件。2.2 MCP 通道配置与参数解析OpenClaw 技能和 MySQL 之间走的是 MCPModel Context Protocol协议也就是说技能对 agent 暴露的是一组有名字、有输入模式、有返回值格式的工具。配置的关键是把数据库连接信息写进环境变量再在技能代码里读取。{ name: mysql-crud, version: 1.0.0, tools: [ { name: query, description: 对 MySQL 执行只读查询返回 JSON 数组, inputSchema: { type: object, properties: { sql: { type: string, description: 完整的 SELECT 语句必须以 SELECT 开头 } }, required: [sql] } } ], mcpServers: { mysql: { command: node, args: [index.ts], env: { MYSQL_HOST: 127.0.0.1, MYSQL_PORT: 3306, MYSQL_USER: app_user, MYSQL_PASSWORD: ${MYSQL_PASSWORD}, MYSQL_DATABASE: business_db } } } }这段配置里最关键的是 inputSchema 和 env 两部分。inputSchema 决定了 agent 调用工具时能传什么参数——你必须把 sql 限制为以 SELECT 开头否则后面做安全控制时压力会很大。env 里我习惯不填真实密码而是用${MYSQL_PASSWORD}占位由 OpenClaw 启动时从宿主环境注入这样技能包里不会泄露凭据。MYSQL_PORT 默认 3306如果你的实例跑在其他端口记得改这一项。2.3 技能的基础执行流程配置只是第一步真正的执行链路是用户输入自然语言需求 → agent 解析并决定调用 mysql-crud 技能 → 技能读取配置里的数据库连接参数 → 执行 SQL 并返回结构化结果。这个链路里最容易翻车的是 agent 生成的 SQL 不合法所以技能实现里我强烈建议加一层语法预检。import { Client } from mysql2/promise; const pool createPool({ host: process.env.MYSQL_HOST, port: Number(process.env.MYSQL_PORT), user: process.env.MYSQL_USER, password: process.env.MYSQL_PASSWORD, database: process.env.MYSQL_DATABASE, waitForConnections: true, connectionLimit: 10, enableKeepAlive: true }); export async function query(sql: string): Promiseunknown { const trimmed sql.trim().toLowerCase(); if (!trimmed.startsWith(select)) { throw new Error(query 工具仅允许 SELECT 语句); } const [rows] await pool.query(sql); return rows; }这里的核心不是pool.query本身而是前置的startsWith(select)校验。它的作用是建立第一道防线——query 工具只负责读不负责写。很多人在这里偷懒把所有 SQL 都放行结果技能就变成了一个 SQL 注入放大器。connectionLimit: 10是连接池上限按 OpenClaw 单实例并发估算10 够用了如果业务量更大改成 20 也不会有什么问题注意别超过 MySQL 的max_connections即可。3. 四个核心工具的实现逻辑面向增删改查逐一拆解3.1 query 只读查询的设计边界上一节看到了 query 的基础版本但在实际技能包里它还有一个隐藏逻辑结果集大小限制。直接执行用户给的 SQL 有一个隐患——如果 agent 生成了没有 WHERE 条件的 SELECT一次返回几十万行MCP 会直接把内存打爆。export async function query(sql: string, limit 200): Promiseunknown { if (!/^select\s/i.test(sql.trim())) { throw new Error(仅允许 SELECT); } // 防止绕过 limit在语句外层包一层 const wrappedSql SELECT * FROM (${sql}) AS _inner LIMIT ${limit}; const [rows] await pool.query(wrappedSql); return { rowCount: (rows as unknown[]).length, rows }; }我在这里用了「外层包 LIMIT」的方式而不是在 SQL 末尾追加LIMIT 200原因是 agent 生成的 SQL 里很可能自带分号或子查询直接拼接字符串会导致语法错误。外层包一层在绝大多数场景下语法都是合法的代价是性能略差——但对于技能调用场景这个开销可以接受。limit参数默认 200是考虑到了 MCP 返回消息体的长度限制OpenClaw 在飞书这类 IM 上输出时长文本非常容易被截断这个坑后面避坑章还会专门说。3.2 insert 工具参数化与自增 ID 回传插入操作和查询最大的不同在于查询允许用户传任意 SQL插入绝对不能。原因很简单——用户传的 INSERT 语句里可能带有ON DUPLICATE KEY UPDATE之类的副作用或者干脆就不是 insert 而是 drop。所以 insert 工具的输入模式不是「SQL 字符串」而是「表名 数据对象」。export async function insert( table: string, data: Recordstring, unknown ): Promiseunknown { const allowedTables [orders, users, payments]; if (!allowedTables.includes(table)) { throw new Error(不允许操作表: ${table}); } const keys Object.keys(data); const values keys.map((k) data[k]); const placeholders keys.map(() ?).join(, ); const sql INSERT INTO ${table} (${keys.join(, )}) VALUES (${placeholders}); const [result] await pool.execute(sql, values); return { insertId: (result as { insertId: number }).insertId }; }这里做了两层限制一是表名必须落在白名单里二是用pool.execute加占位符做参数化杜绝 SQL 注入。很多刚接触这个方向的人会困惑为什么不直接用模板字符串拼值因为 agent 生成的值是不可信的参数化是最稳妥的后悔药。返回值里的insertId在业务场景里非常重要比如插入一条订单后后续操作要拿这个 ID 去关联子表没有它你就得再查一次。3.3 update 工具强制条件与自增 ID 回传update 工具是所有工具里风险最高的因为一旦 agent 忘了写 WHERE全表数据就被改了。我的做法是update 工具不允许 agent 直接传完整 SQL而是要求它传「表名、更新字段对象、WHERE 条件对象」三个独立参数。export async function update( table: string, set: Recordstring, unknown, where: Recordstring, unknown ): Promiseunknown { if (Object.keys(where).length 0) { throw new Error(update 操作必须携带 where 条件); } const setKeys Object.keys(set); const whereKeys Object.keys(where); const setClause setKeys.map((k) ${k} ?).join(, ); const whereClause whereKeys.map((k) ${k} ?).join( AND ); const sql UPDATE ${table} SET ${setClause} WHERE ${whereClause}; const values [...setKeys.map((k) set[k]), ...whereKeys.map((k) where[k])]; const [result] await pool.execute(sql, values); return { affectedRows: (result as { affectedRows: number }).affectedRows }; }关键点在第一道闸门where对象为空就报错。这是硬性约束不依赖 agent 的判断。实际使用中agent 有时会生成WHERE id 1 OR 1 1这种条件所以在 where 条件的 value 上我还习惯加一层类型校验禁止传入字符串形式的裸表达式只允许数字、字符串字面量这类安全值。affectedRows 返回值用于判断更新是否生效如果返回 0说明条件没匹配到数据agent 会基于这个结果向用户解释。3.4 delete 工具先查后删的双步确认delete 工具是在 update 的基础上再做一层心理防线。我要求技能实现里delete 必须走「先 SELECT 验证行数、再 DELETE」的两步流程。export async function remove( table: string, where: Recordstring, unknown ): Promiseunknown { if (!where || Object.keys(where).length 0) { throw new Error(delete 操作必须携带 where 条件); } const whereKeys Object.keys(where); const whereClause whereKeys.map((k) ${k} ?).join( AND ); const values whereKeys.map((k) where[k]); // 第一步查询将影响的行 const [rows] await pool.query( SELECT * FROM ${table} WHERE ${whereClause} LIMIT 5, values ); const count (rows as unknown[]).length; // 第二步如果影响行数超过阈值拒绝执行 if (count 5) { throw new Error(删除影响行数过多本次共 ${count} 行已终止); } const sql DELETE FROM ${table} WHERE ${whereClause}; const [result] await pool.execute(sql, values); return { deletedRows: (result as { deletedRows: number }).affectedRows }; }为什么限制 5 行这是为了逼 agent 走「精确删除」的逻辑——要么条件足够窄要么分批删除。如果业务上确实需要批量清数据我会建议单独写一个批量删除的技能而不是让通用 delete 工具背这个锅。两步流程的副作用是性能变慢但 delete 本身不在高频调用路径上慢一点换安全性这笔账是划算的。4. 连接池与事务封装让技能在真实负载下不崩4.1 连接池参数怎么调OpenClaw 技能跑起来之后连接池参数是最先暴露问题的地方。默认配置里waitForConnections: true意味着并发请求来的时候线程会排队等待空闲连接而connectionLimit设得太小会直接导致超时报错。我在本地压测时默认 10 个连接在并发 20 个请求时就出现明显排队了。export const pool createPool({ host: process.env.MYSQL_HOST, port: Number(process.env.MYSQL_PORT), user: process.env.MYSQL_USER, password: process.env.MYSQL_PASSWORD, database: process.env.MYSQL_DATABASE, waitForConnections: true, connectionLimit: 20, queueLimit: 50, enableKeepAlive: true, keepAliveInitialDelay: 10000, timezone: Z });参数含义拆开说queueLimit: 50表示排队最多 50 个请求再多的直接拒绝避免无限制堆积把内存打满keepAliveInitialDelay: 10000是连接空闲 10 秒后才开始发心跳包防止 MySQL 服务端的wait_timeout把连接回收掉。timezone: Z是我项目中实打实踩过的坑——不设置的话MySQL 的DATETIME类型返回给 agent 时会被本地时区干扰导致时间差 8 小时排查起来极其蛋疼。4.2 事务封装为什么四个工具不能直接叠加把四个独立工具串起来做一件事的时候会遇到一个严重问题每个工具各自会提交自己的事务。比如「把订单状态改成已支付同时扣减库存」——如果第一步成功第二步失败数据就处于中间状态了。export async function runTransaction(operations: Array{ type: insert | update | delete; table: string; data?: Recordstring, unknown; set?: Recordstring, unknown; where?: Recordstring, unknown; }): Promiseunknown { const conn await pool.getConnection(); try { await conn.beginTransaction(); const results []; for (const op of operations) { switch (op.type) { case insert: results.push(await conn.execute( INSERT INTO ${op.table} SET ?, [op.data] )); break; case update: results.push(await conn.execute( UPDATE ${op.table} SET ? WHERE ?, [op.set, op.where] )); break; case delete: results.push(await conn.execute( DELETE FROM ${op.table} WHERE ?, [op.where] )); break; } } await conn.commit(); return results; } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); } }这个事务版本里有个细节INSERT INTO table SET ?是 MySQL 的扩展语法多个字段更新时比传统的(col1, col2) VALUES (?, ?)写法更紧凑代码可读性也好。conn.release()放在finally里保证异常路径上连接也一定归还给连接池——不归还的话池子会被慢慢吃光最后所有请求全部卡死。4.3 只读模式与危险操作拦截很多业务场景下OpenClaw 技能只需要查数据根本不需要写操作。我给技能预留了一个开关环境变量MYSQL_READ_ONLY设成true时insert/update/delete 三个工具直接返回「当前技能为只读模式」。const readOnly process.env.MYSQL_READ_ONLY true; export async function assertWritable(): Promisevoid { if (readOnly) { throw new Error(当前技能配置为只读模式无法执行写操作); } }这个开关帮我挡掉过几次在线事故。实际部署时我会在开发环境开写权限在预发和线上强制开只读——不是不信任 agent而是给自己的手误留后悔药。顺带说一句MYSQL_READ_ONLY这个名字会让人误以为只是改了 SQL 模式实际上它就是在应用层直接掐断写路径MySQL 服务端不改任何配置安全且可随时切换。5. 避坑与常见问题排查session 锁、飞书截断和连接异常5.1 agent failed before reply: session file locked现象OpenClaw 在多个会话同时调用 mysql-crud 技能时间歇性报错agent failed before reply: session file locked (timeout 60000ms)。原因OpenClaw 的会话管理默认对 session 文件加了文件锁同一时刻只有一个进程能读写该 session 文件技能执行耗时长时直接撞锁超时。解决把技能调用方式从「阻塞式会话」改成「单轮非持久化会话」或者在技能代码里减少不必要的 LLM 往返——比如把 SQL 校验逻辑放在本地执行让技能尽可能一次返回别让轮次拉长占用 session 文件的时间。5.2 OpenClaw 在飞书输出容易截断现象agent 返回超过 1 千字的查询结果在飞书机器人对话里被截断用户看到的是一段没有结尾的 JSON。原因飞书消息接口对单条消息有长度限制技能返回了大量数据直接透传给 IM 网关。解决在技能实现里加入结果集压缩超出阈值时不做完整返回而是把数据写入临时表返回一个查询链接或者直接缩小query工具的 limit 默认值。我实践下来500 字左右的精简摘要 结构化关键字段在飞书里的观感最好。5.3 Error 2003: Cant connect to MySQL server on localhost:3306现象OpenClaw 部署在容器里技能配置的 host 是localhost连接数据库时报 2003 错误。原因容器内的 localhost 指向容器自身而不是宿主机或 MySQL 容器。解决如果 MySQL 跑在宿主机上host 改成host.docker.internal如果跑在另一个容器里改成服务名或容器 IP。这个报错排查起来不算难但特别容易在刚上手时被忽略我见过不止一个人在这个坑里卡半小时。5.4 时区错乱导致查询结果差 8 小时现象业务表里的created_at字段在 OpenClaw 返回给用户后时间比数据库里的实际值多了 8 小时。原因MySQL JDBC 驱动和 mysql2 连接池的默认时区是服务器本地时区而 OpenClaw 运行环境是 UTC。解决在连接参数里显式指定timezone: Z或者更彻底一点——数据库连接字符串里加上?useTimezonetrueserverTimezoneUTC。我之前的做法是前者因为它对技能代码侵入最小但如果你用 JDBC 那套链路还是得确认连接串正确。5.5 删除操作误伤全表现象测试环境中agent 生成了一条没有 WHERE 的 DELETE 语句直接把整张业务表清空。原因技能工具没有对 SQL 做条件强制校验agent 输出了不良 SQL 被直接执行。解决在技能实现里按上文提到的 delete 工具版本「先查后删」并且限制单次删除行数。另外还有一个兜底手段连接数据库的账号只授予DELETE权限不授予DROP权限这样即使 agent 发疯最多删行、不能删表。这两条同时生效是我目前见过的最高性价比方案。5.6 OpenClaw 与 WorkBuddy 的选择现象团队在评估 agent 框架时在 OpenClaw 和 WorkBuddy 之间犹豫认为 OpenClaw 安装复杂。原因网上教程混杂且 OpenClaw 的技能定义和 MCP 配置要求开发者理解协议本身学习曲线比 WorkBuddy 的图形化操作陡峭一些。解决如果你已经要接 MySQL 这类外部系统建议直接上 OpenClaw——因为技能是纯代码定义版本可控出了问题可以直接看日志定位WorkBuddy 的图形化配置在简单场景下省事但到了要精细控制 SQL 边界时反而没有代码来得透明。如果团队里有人已经会写 Node 或 Python选 OpenClaw 更稳。6. 进阶验证用模拟数据和高并发检查技能健壮性技能写完了怎么确认它能扛住真实负载我的习惯是分三步验证先灌模拟数据看基本行为再压并发看连接池最后做错误注入看恢复能力。# 造一张简单的业务表并插入 5 万条测试数据 CREATE TABLE demo_orders ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_id (user_id) ); -- 用存储过程批量插入模拟真实数据量 DELIMITER $$ CREATE PROCEDURE seed_orders() BEGIN DECLARE i INT DEFAULT 0; WHILE i 50000 DO INSERT INTO demo_orders (user_id, amount, status) VALUES (FLOOR(RAND() * 1000), RAND() * 1000, FLOOR(RAND() * 3)); SET i i 1; END WHILE; END$$ DELIMITER ; CALL seed_orders();数据就位后用两种方式验证一是通过自然语言让 agent 查询「status 1 的订单总金额是多少」看 SQL 生成是否正确二是直接调技能工具压测并发。压测我一般不用 ab而是用 node 脚本并发跑 query 工具 200 次。重点观察两个指标连接池是否稳定在预设上限、是否有大量queueLimit exceeded报错。前者检查配置是否合理后者暴露并发尖峰时的瓶颈。错误注入是最后一道关。我会故意传错误的表名、错误的 SQL、带有 WHERE 11 的 delete 请求看技能是抛出可读的报错信息还是直接把堆栈交给 agent。好技能的标准是报错信息能引导 agent 修正输入而不是让 agent 陷入「反复生成同一条错误 SQL」的死循环。这需要你在工具实现的错误处理里针对ER_PARSE_ERROR和ER_NO_SUCH_TABLE这类常见 MySQL 错误号给中文提示。从那以后我每次新增或修改技能都强制走一遍「灌数据 → 压并发 → 注入错误」的自检流程不再凭「这段代码跑通了」就交付。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑