diff --git a/.kilo/plans/lzwcai-agile-db-skill.md b/.kilo/plans/lzwcai-agile-db-skill.md new file mode 100644 index 0000000..9134d36 --- /dev/null +++ b/.kilo/plans/lzwcai-agile-db-skill.md @@ -0,0 +1,451 @@ +# lzwcai-agile-db Skill 实现计划 + +## 目标 +创建一个 Kilo skill 文件,为 AI Agent 提供 AgileDB 数据库操作的场景化工作流指导。 + +## Skill 文件路径 +`.kilo/skills/lzwcai-agile-db/SKILL.md` + +## Skill 内容 + +以下为完整的 SKILL.md 内容: + +```markdown +# lzwcai-agile-db + +AgileDB 数据库操作技能。为 AI Agent 提供数据源浏览、表数据 CRUD、SQL 执行和 AI 生成表结构的场景化工作流。 + +## 可用 MCP 工具 + +本 skill 基于 `lzwcai_mcp_agile_db` MCP Server,提供以下工具: + +### 数据源管理 +| 工具 | 功能 | +|------|------| +| `list_datasources` | 获取数据源列表 | +| `get_datasource_detail` | 获取数据源详情(含数据库、表结构) | +| `create_datasource` | 创建外部数据源 | +| `update_datasource` | 更新数据源 | +| `toggle_datasource_status` | 启用/停用数据源 | +| `delete_datasource` | 删除数据源 | + +### 数据库与表管理 +| 工具 | 功能 | +|------|------| +| `list_databases` | 获取数据源下的数据库列表 | +| `list_tables` | 获取数据库下的表列表 | +| `get_table_detail` | 获取表结构详情(字段、类型、主键) | +| `create_table` | 创建新表 | +| `alter_table` | 修改表结构 | +| `generate_table_by_description` | AI 根据自然语言生成表结构 | + +### 表数据操作 +| 工具 | 功能 | +|------|------| +| `query_table_data` | 查询表数据(分页) | +| `insert_table_row` | 插入一行数据 | +| `update_table_row` | 更新一行数据 | +| `delete_table_rows` | 删除数据行(按主键) | +| `export_table_excel` | 导出表数据为 Excel | + +### SQL 执行 +| 工具 | 功能 | +|------|------| +| `execute_sql` | 执行原生 SQL 查询 | + +--- + +## 场景 1:浏览数据源 + +当用户想要了解有哪些数据源、数据库或表时,使用此流程。 + +### 工作流程 + +``` +用户请求: "有哪些数据源?" / "看看 XX 数据源有哪些表?" + ↓ +1. 调用 list_datasources() + ↓ +2. 展示数据源列表(名称、类型、状态、数据库数、表数) + ↓ +3. 用户选择数据源后,调用 get_datasource_detail(datasourceId="xx") + ↓ +4. 展示数据库列表和实时表结构 +``` + +### 示例 + +**用户**: "帮我看看有哪些数据源" + +``` +调用: list_datasources() +返回: [{id: "1", name: "订单系统", type: "builtin", status: 0, databases: 3, tables: 12}, ...] +回复: 共找到 3 个数据源: + - 订单系统(内置,运行中,3 个库,12 张表) + - 用户中心(外部 MySQL,运行中,1 个库,5 张表) + - 报表系统(外部 PostgreSQL,已停止) +``` + +**用户**: "看看订单系统有哪些表" + +``` +调用: get_datasource_detail(datasourceId="1") +返回: {datasource: {...}, databases: [{name: "order_db", tables: [...]}]} +回复: 订单系统包含以下数据库: + order_db: + - orders (订单表, 15 个字段) + - order_items (订单明细, 10 个字段) + - users (用户表, 8 个字段) +``` + +### 注意事项 +- 数据源状态:`0`=运行中,`1`=已停止 +- 数据源类型:`builtin`=内置,`external`=外部 +- 如果用户只说"查一下数据库",先调用 `list_datasources()` 再引导选择 + +--- + +## 场景 2:查询表数据 + +当用户想要查看某张表的具体数据记录时,使用此流程。 + +### 工作流程 + +``` +用户请求: "查一下 users 表的数据" / "看看前 20 条订单" + ↓ +1. 如果不知道 tableId,先浏览数据源找到目标表 + ↓ +2. 调用 query_table_data(tableId="xx", pageNum=1, pageSize=10, target="prod") + ↓ +3. 展示数据(表格形式,包含字段名和值) + ↓ +4. 如果数据量大,提示用户可翻页或调整 pageSize +``` + +### 示例 + +**用户**: "查一下 users 表前 10 条数据" + +``` +调用: query_table_data(tableId="5", pageNum=1, pageSize=10) +返回: { + columns: [{name: "id", type: "INTEGER"}, {name: "username", type: "VARCHAR"}, ...], + data: [{id: 1, username: "admin", email: "admin@test.com"}, ...], + total: 156 +} +回复: users 表共 156 条记录,当前显示第 1-10 条: + | id | username | email | created_at | + |----|----------|-----------------|---------------------| + | 1 | admin | admin@test.com | 2024-01-15 10:30:00 | + | 2 | user1 | user1@test.com | 2024-01-16 14:20:00 | + ... +``` + +### 注意事项 +- `target` 参数:`prod`=生产环境,`test`=测试环境,默认 `prod` +- 如果用户未指定数量,默认 `pageSize=10` +- 数据返回格式为对象数组(已自动转换表头) +- 如果用户说"翻页",增加 `pageNum` 参数 + +--- + +## 场景 3:执行 SQL + +当用户需要执行自定义 SQL 查询时,使用此流程。 + +### 工作流程 + +``` +用户请求: "帮我执行 SQL..." / "查一下订单数大于 100 的用户" + ↓ +1. 如果用户直接提供 SQL,直接使用 + 如果用户提供自然语言需求,先转换为 SQL + ↓ +2. 调用 execute_sql(datasourceId="xx", executableSql="SELECT ...") + ↓ +3. 展示查询结果 + ↓ +4. 如果 SQL 执行失败,展示错误信息并建议修正 +``` + +### 示例 + +**用户**: "统计每个地区的订单数量" + +``` +转换为 SQL: SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC + +调用: execute_sql( + datasourceId="1", + executableSql="SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC" +) +返回: { + columns: [{name: "region"}, {name: "order_count"}], + data: [{region: "华东", order_count: 1250}, {region: "华南", order_count: 980}, ...] +} +回复: 各地区订单统计: + | 地区 | 订单数 | + |------|--------| + | 华东 | 1,250 | + | 华南 | 980 | + | 华北 | 756 | +``` + +### 注意事项 +- `datasourceId` 必须提供,如果用户未指定,先询问或使用最近使用的数据源 +- `sqlTemplate` 可选,用于模板化查询 +- `parameters` 可选,用于参数化查询(防止 SQL 注入) +- 执行危险操作(DELETE/DROP/UPDATE)前,**必须**向用户确认 +- 如果查询结果超过 100 行,建议用户使用 `query_table_data` 代替 + +--- + +## 场景 4:增删改数据 + +当用户需要修改表中的数据时,使用此流程。 + +### 4.1 插入数据 + +``` +用户请求: "新增一个用户,用户名是 test_user" + ↓ +1. 先调用 get_table_detail(tableId="xx") 了解表结构 + ↓ +2. 确认必填字段(非空字段、无默认值字段) + ↓ +3. 调用 insert_table_row(tableId="xx", data={...}) + ↓ +4. 确认插入成功 +``` + +**示例**: + +**用户**: "新增一个用户,用户名 test_user,邮箱 test@test.com" + +``` +调用: get_table_detail(tableId="5") +返回: {columns: [{name: "id", isPrimaryKey: true, isAutoIncrement: true}, {name: "username", isNullable: false}, ...]} + +调用: insert_table_row( + tableId="5", + data={"username": "test_user", "email": "test@test.com"} +) +回复: 已成功插入用户 test_user (ID 自动生成) +``` + +### 4.2 更新数据 + +``` +用户请求: "把用户 test_user 的邮箱改成 new@test.com" + ↓ +1. 确认主键字段和要更新的值 + ↓ +2. 调用 update_table_row(tableId="xx", primaryKey={主键}, data={要更新的字段}) + ↓ +3. 确认更新成功 +``` + +**示例**: + +**用户**: "把 ID 为 5 的用户邮箱改成 new@test.com" + +``` +调用: update_table_row( + tableId="5", + primaryKey={"id": 5}, + data={"email": "new@test.com"} +) +回复: 已更新用户 ID=5 的邮箱为 new@test.com +``` + +### 4.3 删除数据 + +``` +用户请求: "删除用户 test_user" + ↓ +1. ⚠️ 危险操作!先确认删除范围和影响 + ↓ +2. 向用户展示将要删除的数据,请求确认 + ↓ +3. 用户确认后,调用 delete_table_rows(tableId="xx", primaryKeys=[{主键}]) + ↓ +4. 确认删除成功 +``` + +**示例**: + +**用户**: "删除 ID 为 10 的订单" + +``` +回复: ⚠️ 确认要删除以下记录吗? + 订单 ID=10, 订单号=ORD-2024-0010, 金额=¥1,250.00 + 此操作不可恢复。请回复"确认删除"继续。 + +用户: "确认删除" + +调用: delete_table_rows( + tableId="3", + primaryKeys=[{"id": 10}] +) +回复: 已成功删除订单 ID=10 +``` + +### 注意事项 +- **删除操作必须二次确认** +- `primaryKey` 必须是对象格式,如 `{"id": 1}` +- `data` 只包含要更新的字段,不需要提供全部字段 +- 插入数据时,自增主键不需要提供 +- 如果操作涉及多行,使用批量操作或循环调用 + +--- + +## 场景 5:AI 生成表结构 + +当用户需要创建新表但不知道如何设计表结构时,使用 AI 辅助生成。 + +### 工作流程 + +``` +用户请求: "我需要一个用户表" / "帮我设计一个订单系统的表结构" + ↓ +1. 调用 generate_table_by_description(requirement="用户描述") + ↓ +2. 展示 AI 生成的表结构(表名、字段、类型、注释) + ↓ +3. 用户确认后,调用 create_table(connectionId="xx", databaseName="xx", tableName="xx", columns=[...]) + ↓ +4. 确认创建成功 +``` + +### 示例 + +**用户**: "帮我设计一个商品表,包含名称、价格、库存、分类" + +``` +调用: generate_table_by_description( + requirement="我需要一个商品表,包含商品名称、价格、库存、分类,用于电商系统" +) +返回: { + tableName: "products", + tableComment: "商品表", + columns: [ + {columnName: "id", columnType: "SERIAL", isPrimaryKey: true, isAutoIncrement: true, columnComment: "主键ID"}, + {columnName: "name", columnType: "VARCHAR", columnLength: 200, isNullable: false, columnComment: "商品名称"}, + {columnName: "price", columnType: "DECIMAL", columnLength: 10, columnComment: "价格"}, + {columnName: "stock", columnType: "INTEGER", columnComment: "库存数量"}, + {columnName: "category_id", columnType: "INTEGER", columnComment: "分类ID"}, + {columnName: "created_at", columnType: "TIMESTAMP", defaultValue: "NOW()", columnComment: "创建时间"} + ] +} + +回复: AI 已生成表结构设计: + 表名: products (商品表) + 字段: + - id (SERIAL, 主键, 自增) - 主键ID + - name (VARCHAR(200), 非空) - 商品名称 + - price (DECIMAL(10)) - 价格 + - stock (INTEGER) - 库存数量 + - category_id (INTEGER) - 分类ID + - created_at (TIMESTAMP, 默认NOW()) - 创建时间 + + 是否需要在此基础上修改或直接创建? + +用户: "直接创建到 order_db 数据库" + +调用: create_table( + connectionId="1", + databaseName="order_db", + tableName="products", + tableComment="商品表", + columns=[...] // 使用 AI 生成的 columns +) +回复: 已成功创建表 products (商品表) +``` + +### 注意事项 +- `requirement` 参数应尽可能详细,包含业务场景和字段需求 +- AI 生成的表结构可能需要用户调整(如字段长度、类型) +- 创建表前需要确认 `connectionId` 和 `databaseName` +- 常用字段类型:`VARCHAR`, `INTEGER`, `SERIAL`(自增主键), `DECIMAL`, `TIMESTAMP`, `TEXT`, `BOOLEAN` +- 如果用户描述模糊,先引导用户补充细节 + +--- + +## 最佳实践 + +### 1. 始终确认环境 +- 默认使用 `prod`(生产环境) +- 如果用户提到"测试"或"测试环境",使用 `target="test"` +- 执行危险操作前,明确告知用户当前环境 + +### 2. 提供上下文 +- 展示数据时,包含字段名和类型 +- 执行操作后,告知影响的行数或具体变化 +- 错误时,提供清晰的错误信息和建议 + +### 3. 分步引导 +- 如果用户请求不完整(如未指定数据源),先引导补充信息 +- 复杂操作分步执行,每步确认后继续 +- 对于多表操作,先展示整体计划 + +### 4. 数据展示 +- 表格数据使用 Markdown 表格格式 +- 长文本截断显示(最多 100 字符) +- 数字格式化(千位分隔符、货币符号) +- 时间格式化为可读格式 + +### 5. 错误处理 +- API 错误:展示错误信息,建议重试或检查参数 +- SQL 错误:展示 SQL 错误位置,建议修正 +- 连接错误:检查数据源状态,建议启用或重新配置 + +--- + +## 常用参数参考 + +### 数据源类型 +- `builtin` - 内置 PostgreSQL +- `external` - 外部数据库(MySQL/PostgreSQL/Oracle/SQL Server 等) + +### 数据源状态 +- `0` - 运行中 +- `1` - 已停止 + +### 环境 +- `prod` - 生产环境(默认) +- `test` - 测试环境 + +### 常用字段类型 +| 类型 | 用途 | 示例 | +|------|------|------| +| `SERIAL` | 自增主键 | `id SERIAL PRIMARY KEY` | +| `VARCHAR(n)` | 短文本 | `username VARCHAR(50)` | +| `TEXT` | 长文本 | `description TEXT` | +| `INTEGER` | 整数 | `stock INTEGER` | +| `DECIMAL(m,n)` | 精确小数 | `price DECIMAL(10,2)` | +| `TIMESTAMP` | 时间戳 | `created_at TIMESTAMP` | +| `BOOLEAN` | 布尔值 | `is_active BOOLEAN` | + +--- + +## 快速开始 + +如果用户说"帮我查一下数据库",按以下步骤操作: + +1. 调用 `list_datasources()` 获取数据源列表 +2. 展示列表,让用户选择或默认第一个运行中的数据源 +3. 调用 `get_datasource_detail(datasourceId="xx")` 获取数据库和表信息 +4. 引导用户选择要操作的表 +5. 根据用户意图调用相应的工具(查询/执行 SQL/增删改) +``` + +## 实施步骤 + +1. 创建目录 `.kilo/skills/lzwcai-agile-db/` +2. 创建文件 `SKILL.md` 并写入上述内容 +3. 验证 skill 文件格式正确 + +## 验证 + +创建完成后,skill 应该可以被 Kilo 自动识别和加载。 diff --git a/.kilo/skills/lzwcai-agile-db.zip b/.kilo/skills/lzwcai-agile-db.zip new file mode 100644 index 0000000..72157c8 Binary files /dev/null and b/.kilo/skills/lzwcai-agile-db.zip differ diff --git a/.kilo/skills/lzwcai-agile-db/SKILL.md b/.kilo/skills/lzwcai-agile-db/SKILL.md new file mode 100644 index 0000000..16f04f9 --- /dev/null +++ b/.kilo/skills/lzwcai-agile-db/SKILL.md @@ -0,0 +1,893 @@ +--- +name: lzwcai-agile-db +description: AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据库操作工作流指导,适合零基础用户。 +version: 0.2.0 +--- + +# lzwcai-agile-db + +AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据库操作工作流指导,适合零基础用户。 + + +## 完整工具清单(33 个工具) + +本 skill 基于 `lzwcai_mcp_agile_db` MCP Server,共提供 33 个工具,分为 9 大类: + +### 一、数据源管理(6 个工具) + +| 工具 | 功能 | 危险等级 | +|------|------|----------| +| `list_datasources` | 获取数据源列表 | 安全 | +| `get_datasource_detail` | 获取数据源详情(含数据库、表结构) | 安全 | +| `create_datasource` | 创建外部数据源连接 | 安全 | +| `update_datasource` | 更新数据源连接信息 | 中等 | +| `toggle_datasource_status` | 启用/停用数据源 | 中等 | +| `delete_datasource` | 删除数据源 | **危险** | + +### 二、数据库与表管理(6 个工具) + +| 工具 | 功能 | 危险等级 | +|------|------|----------| +| `list_databases` | 获取数据源下的数据库列表 | 安全 | +| `list_tables` | 获取数据源下的表列表 | 安全 | +| `get_table_detail` | 获取表结构详情(字段、类型、主键) | 安全 | +| `create_table` | 创建新表 | **危险** | +| `alter_table` | 修改表结构 | **危险** | +| `generate_table_by_description` | AI 根据自然语言生成表结构 | 安全(仅生成,不创建) | + +### 三、表数据 CRUD(5 个工具) + +| 工具 | 功能 | 危险等级 | +|------|------|----------| +| `query_table_data` | 查询表数据(分页) | 安全 | +| `insert_table_row` | 插入一行数据 | 中等 | +| `update_table_row` | 更新一行数据 | 中等 | +| `delete_table_rows` | 删除数据行(按主键) | **危险** | +| `export_table_excel` | 导出表数据为 Excel(base64) | 安全 | + +### 四、SQL 执行(1 个工具) + +| 工具 | 功能 | 危险等级 | +|------|------|----------| +| `execute_sql` | 执行原生 SQL 查询 | 中等/危险 | + +### 五、数据导入(2 个工具) + +| 工具 | 功能 | 危险等级 | +|------|------|----------| +| `preview_import_data` | 上传 Excel 文件,AI 智能识别并预览 | 安全 | +| `confirm_import_data` | 确认导入 AI 识别后的数据 | **危险** | + +### 六、表订阅(1 个工具) + +| 工具 | 功能 | 危险等级 | +|------|------|----------| +| `toggle_table_subscription` | 切换表的订阅状态 | 中等 | + +### 七、API 密钥管理(6 个工具) + +| 工具 | 功能 | 危险等级 | +|------|------|----------| +| `list_api_keys` | 获取 API 密钥列表 | 安全 | +| `create_api_key` | 创建新的 API 密钥 | 中等 | +| `toggle_api_key_status` | 启用/禁用 API 密钥 | 中等 | +| `delete_api_key` | 删除 API 密钥 | **危险** | +| `get_api_key_permissions` | 查看指定密钥的权限配置 | 安全 | +| `grant_api_key_permissions` | 批量为 API 密钥授予权限 | **危险** | + +### 八、技能与工具管理(6 个工具) + +| 工具 | 功能 | 危险等级 | +|------|------|----------| +| `get_skill_by_datasource` | 根据数据源获取技能信息 | 安全 | +| `get_skill_tools` | 获取技能下的工具列表 | 安全 | +| `create_skill` | 为数据源创建技能 | 中等 | +| `create_sql_tool` | 将 SQL 查询创建为可复用工具 | 中等 | +| `delete_skill_tool` | 删除技能下的工具 | **危险** | +| `update_skill_config` | 更新技能配置 | 中等 | + +--- + +## 核心概念说明 + +### 数据源是什么? + +数据源 = 一个数据库连接。它可以是: +- `builtin`:内置 PostgreSQL 数据库(系统自带) +- `external`:外部数据库(MySQL、PostgreSQL、Oracle、SQL Server、达梦等) + +### 数据源状态 + +- `0` = 运行中(正常) +- `1` = 已停止(不可用) + +### 环境参数 `target` + +- `prod` = 生产环境(正式数据,默认值) +- `test` = 测试环境(测试数据) + +### 主键 `primaryKey` + +主键是唯一标识一行数据的字段。例如 `{"id": 1}` 表示删除/更新 id=1 的那行数据。 + +### `connectionId` vs `datasourceId` + +- `datasourceId`:用于数据源列表、数据库列表、表列表等浏览类操作 +- `connectionId`:用于创建表、修改表、数据导入等需要直接操作连接的操作 +- 两者通常指向同一个东西,但 API 设计不同,请按照工具说明使用 + +--- + +## ⚠️ 安全确认原则(必须遵守) + +### 一、执行工具前的风险评估 + +当执行以下类型的操作时,**必须先询问用户确认**,不得擅作主张: + +1. **删除操作**:删除数据源、删除表数据、删除 API 密钥、删除技能工具等 + - 说明:此操作不可恢复,数据将永久丢失 + +2. **泄密风险操作**:导出包含敏感数据的表、创建 API 密钥、查看密钥详情等 + - 说明:可能导致敏感信息泄露,需确认用户授权 + +3. **政治敏感操作**:涉及政治相关数据的查询、修改、删除等 + - 说明:可能涉及合规风险,需确认用户意图 + +4. **疑似违规内容**:涉及色情、暴力、违法等内容的操作 + - 说明:可能违反法律法规,必须拒绝执行并告知用户 + +#### 确认格式 + +执行上述操作前,必须使用以下格式向用户确认: + +``` +⚠️ 安全提醒:此操作存在 [具体风险类型] 风险。 +具体说明:[说明可能的后果] +请确认是否继续?(回复"确认"继续,或取消操作) +``` + +**只有在用户明确确认后才能继续执行。** + +--- + +### 二、多步骤场景必须逐步确认 + +当完成任务需要调用多个工具时(如:先查询表结构 → 插入数据 → 确认结果),**不得一次性自动连续执行**: + +1. **每完成一步后暂停**,向用户展示当前步骤的结果 +2. **询问用户是否继续下一步**,等待确认后再执行 +3. **不要假设用户的意图**,即使用户的请求看似明确,也需要分步确认 + +**示例**: +``` +用户:"帮我新增一个用户" + +❌ 错误做法:自动调用 get_table_detail → insert_table_row → 返回结果(一次性执行完) + +✅ 正确做法: + 1. 调用 get_table_detail 了解表结构 + 2. 展示必填字段清单,询问用户:"请提供以下必填字段的值:..." + 3. 用户回复后,展示将要插入的数据预览 + 4. 询问:"确认插入以上数据?" + 5. 用户确认后才调用 insert_table_row +``` + +--- + +### 三、遇到多项选择时必须询问用户 + +当执行任务过程中遇到需要选择的场景时,**AI 不得擅自做主选择**: + +1. **列出所有可选方案**,说明每个方案的优缺点或适用场景 +2. **等待用户明确选择**,不要猜测用户意图 +3. **不要默认选择第一个**或看似最合理的选项 + +#### 常见需要询问的选择场景: +- 有多个数据源可选时 +- 有多个数据库可选时 +- 有多个表可选时 +- 有多种操作方式可选时(如:用 SQL 查询 vs 用表数据查询工具) +- 有多种字段类型可选时 +- 用户描述模糊,存在多种理解方式时 + +**示例**: +``` +用户:"查一下订单数据" + +❌ 错误做法:直接选择第一个数据源和第一个订单表进行查询 + +✅ 正确做法: + "找到以下数据源包含订单相关表: + 1. HMD产品 → order_db → orders 表(156 条记录) + 2. 测试数据源 → test_db → test_orders 表(10 条记录) + 请问您想查询哪个?" +``` + +--- + +## 场景 1:浏览数据源(新手入门第一步) + +当用户想了解系统里有哪些数据库或表时,使用此流程。 + +### 工作流程 + +``` +用户请求: "有哪些数据源?" / "看看 XX 数据源有哪些表?" / "帮我查一下数据库" + ↓ +1. 调用 list_datasources() + ↓ +2. 展示数据源列表(名称、类型、状态、数据库数、表数) + ↓ +3. 用户选择数据源后,调用 get_datasource_detail(datasourceId="xx") + ↓ +4. 展示数据库列表和实时表结构 +``` + +### 示例 + +**用户**: "帮我看看有哪些数据源" + +``` +调用: list_datasources() +返回: { + "total": 14, + "rows": [ + {"id": "58", "datasourceName": "HMD产品", "host": "host.docker.internal", "port": 5432, "status": 0, ...}, + ... + ] +} +回复: 共找到 14 个数据源: + 1. HMD产品(PostgreSQL, host.docker.internal:5432, 运行中) + 2. ... + 请告诉我你想看哪个数据源? +``` + +**用户**: "看看 HMD 产品有哪些表" + +``` +调用: get_datasource_detail(datasourceId="58") +返回: { + "detail": {...}, + "config": {...}, + "structure": { + "databases": [ + {"name": "order_db", "tables": [ + {"tableName": "orders", "columns": [...]}, + ... + ]} + ] + } +} +回复: HMD产品 数据源包含以下数据库和表: + order_db: + - orders (订单表, 15 个字段) + - users (用户表, 8 个字段) +``` + +### 注意事项 +- 数据源类型为 `builtin` 表示系统内置数据库,类型为具体的数据库引擎名称(mysql、postgresql 等)表示外部数据源 +- 如果用户只说"查一下数据库",先调用 `list_datasources()` 再引导选择 +- 已停止的数据源(status=1)无法访问其结构 + +--- + +## 场景 2:查询表数据 + +当用户想要查看某张表的具体数据记录时,使用此流程。 + +### 工作流程 + +``` +用户请求: "查一下 users 表的数据" / "看看前 20 条订单" + ↓ +1. 如果不知道 tableId,先浏览数据源找到目标表 + ↓ +2. 调用 query_table_data(tableId="xx", pageNum=1, pageSize=10, target="prod") + ↓ +3. 展示数据(表格形式,包含字段名和值) + ↓ +4. 如果数据量大,提示用户可翻页或调整 pageSize +``` + +### 示例 + +**用户**: "查一下 users 表前 10 条数据" + +``` +调用: query_table_data(tableId="5", pageNum=1, pageSize=10) +返回: { + "columns": [{"name": "id", "type": "INTEGER"}, {"name": "username", "type": "VARCHAR"}, ...], + "data": [{"id": 1, "username": "admin", "email": "admin@test.com"}, ...], + "total": 156 +} +回复: users 表共 156 条记录,当前显示第 1-10 条: + | id | username | email | created_at | + |----|----------|-----------------|---------------------| + | 1 | admin | admin@test.com | 2024-01-15 10:30:00 | + | 2 | user1 | user1@test.com | 2024-01-16 14:20:00 | + ... +``` + +### 注意事项 +- `target` 参数:`prod`=生产环境(默认),`test`=测试环境 +- 如果用户未指定数量,默认 `pageSize=10` +- 如果用户说"翻页",增加 `pageNum` 参数 +- 如果用户想看更多数据,可以增大 `pageSize`(最大根据 API 限制) +- 数据返回格式为对象数组 + +--- + +## 场景 3:执行 SQL + +当用户需要执行自定义 SQL 查询时,使用此流程。 + +### 工作流程 + +``` +用户请求: "帮我执行 SQL..." / "查一下订单数大于 100 的用户" + ↓ +1. 如果用户直接提供 SQL,直接使用 + 如果用户提供自然语言需求,先转换为 SQL + ↓ +2. 调用 execute_sql(datasourceId="xx", executableSql="SELECT ...") + ↓ +3. 展示查询结果 + ↓ +4. 如果 SQL 执行失败,展示错误信息并建议修正 +``` + +### 示例 + +**用户**: "统计每个地区的订单数量" + +``` +转换为 SQL: SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC + +调用: execute_sql( + datasourceId="58", + executableSql="SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC" +) +返回: { + "columns": [{"name": "region"}, {"name": "order_count"}], + "data": [{"region": "华东", "order_count": 1250}, {"region": "华南", "order_count": 980}, ...] +} +回复: 各地区订单统计: + | 地区 | 订单数 | + |------|--------| + | 华东 | 1,250 | + | 华南 | 980 | + | 华北 | 756 | +``` + +### 注意事项 +- `datasourceId` 必须提供,如果用户未指定,先询问 +- `sqlTemplate` 可选,用于模板化查询 +- `parameters` 可选,用于参数化查询(防止 SQL 注入) +- **执行危险操作(DELETE/DROP/TRUNCATE)前,必须向用户确认** +- 如果查询结果超过 100 行,建议用户使用 `query_table_data` 代替 +- 对于只读查询(SELECT),可以直接执行;对于写操作(INSERT/UPDATE/DELETE),必须二次确认 + +--- + +## 场景 4:增删改数据 + +当用户需要修改表中的数据时,使用此流程。 + +### 4.1 插入数据 + +``` +用户请求: "新增一个用户,用户名是 test_user" + ↓ +1. 先调用 get_table_detail(tableId="xx") 了解表结构 + ↓ +2. 确认必填字段(非空字段、无默认值字段) + ↓ +3. 调用 insert_table_row(tableId="xx", data={...}) + ↓ +4. 确认插入成功 +``` + +**示例**: + +**用户**: "新增一个用户,用户名 test_user,邮箱 test@test.com" + +``` +调用: get_table_detail(tableId="5") +返回: {columns: [{name: "id", isPrimaryKey: true, isAutoIncrement: true}, {name: "username", isNullable: false}, ...]} + +调用: insert_table_row( + tableId="5", + data={"username": "test_user", "email": "test@test.com"} +) +回复: 已成功插入用户 test_user (ID 自动生成) +``` + +### 4.2 更新数据 + +``` +用户请求: "把用户 test_user 的邮箱改成 new@test.com" + ↓ +1. 确认主键字段和要更新的值 + ↓ +2. 调用 update_table_row(tableId="xx", primaryKey={主键}, data={要更新的字段}) + ↓ +3. 确认更新成功 +``` + +**示例**: + +**用户**: "把 ID 为 5 的用户邮箱改成 new@test.com" + +``` +调用: update_table_row( + tableId="5", + primaryKey={"id": 5}, + data={"email": "new@test.com"} +) +回复: 已更新用户 ID=5 的邮箱为 new@test.com +``` + +### 4.3 删除数据 + +``` +用户请求: "删除用户 test_user" + ↓ +1. ⚠️ 安全提醒:此操作存在删除风险。 + 说明:数据将永久删除,不可恢复。 + 请确认是否继续? + ↓ +2. ⚠️ 危险操作!先确认删除范围和影响 + ↓ +3. 向用户展示将要删除的数据,请求确认 + ↓ +4. 用户确认后,调用 delete_table_rows(tableId="xx", primaryKeys=[{主键}]) + ↓ +5. 确认删除成功 +``` + +**示例**: + +**用户**: "删除 ID 为 10 的订单" + +``` +回复: ⚠️ 确认要删除以下记录吗? + 订单 ID=10, 订单号=ORD-2024-0010, 金额=¥1,250.00 + 此操作不可恢复。请回复"确认删除"继续。 + +用户: "确认删除" + +调用: delete_table_rows( + tableId="3", + primaryKeys=[{"id": 10}] +) +回复: 已成功删除订单 ID=10 +``` + +**注意:如果删除内容涉及敏感数据(如用户隐私、政治相关内容、疑似违规内容),必须额外说明后果并再次确认。** + +### 4.4 导出表数据为 Excel + +``` +用户请求: "把 users 表数据导出成 Excel" + ↓ +1. 调用 export_table_excel(tableId="xx", target="prod") + ↓ +2. 返回 base64 编码的 Excel 文件内容 + ↓ +3. 提示用户解码 base64 获取文件 +``` + +**示例**: + +``` +调用: export_table_excel(tableId="5") +返回: { + "success": true, + "file_base64": "UEsDBBQAAAAI...", + "message": "Excel 文件已导出,请解码 base64 内容获取文件" +} +回复: 已成功导出 users 表数据为 Excel 文件 + 文件内容已 base64 编码,请解码后保存为 .xlsx 文件 +``` + +### 注意事项 +- **删除操作必须二次确认** +- `primaryKey` 必须是对象格式,如 `{"id": 1}` +- `primaryKeys` 是数组格式,如 `[{"id": 1}, {"id": 2}]` +- `data` 只包含要更新的字段,不需要提供全部字段 +- 插入数据时,自增主键不需要提供 +- 如果操作涉及多行,使用批量操作或循环调用 +- 增删改操作默认作用于 `prod` 环境 + +--- + +## 场景 5:AI 生成表结构 + +当用户需要创建新表但不知道如何设计表结构时,使用 AI 辅助生成。 + +### 工作流程 + +``` +用户请求: "我需要一个用户表" / "帮我设计一个订单系统的表结构" + ↓ +1. 调用 generate_table_by_description(requirement="用户描述") + ↓ +2. 展示 AI 生成的表结构(表名、字段、类型、注释) + ↓ +3. 用户确认后,调用 create_table(connectionId="xx", databaseName="xx", tableName="xx", columns=[...]) + ↓ +4. 确认创建成功 +``` + +### 示例 + +**用户**: "帮我设计一个商品表,包含名称、价格、库存、分类" + +``` +调用: generate_table_by_description( + requirement="我需要一个商品表,包含商品名称、价格、库存、分类,用于电商系统" +) +返回: { + "tableName": "products", + "tableComment": "商品表", + "columns": [ + {"columnName": "id", "columnType": "SERIAL", "isPrimaryKey": true, "isAutoIncrement": true, "columnComment": "主键ID"}, + {"columnName": "name", "columnType": "VARCHAR", "columnLength": 200, "isNullable": false, "columnComment": "商品名称"}, + {"columnName": "price", "columnType": "DECIMAL", "columnLength": 10, "columnComment": "价格"}, + {"columnName": "stock", "columnType": "INTEGER", "columnComment": "库存数量"}, + {"columnName": "category_id", "columnType": "INTEGER", "columnComment": "分类ID"}, + {"columnName": "created_at", "columnType": "TIMESTAMP", "defaultValue": "NOW()", "columnComment": "创建时间"} + ] +} + +回复: AI 已生成表结构设计: + 表名: products (商品表) + 字段: + - id (SERIAL, 主键, 自增) - 主键ID + - name (VARCHAR(200), 非空) - 商品名称 + - price (DECIMAL(10)) - 价格 + - stock (INTEGER) - 库存数量 + - category_id (INTEGER) - 分类ID + - created_at (TIMESTAMP, 默认NOW()) - 创建时间 + + 是否需要在此基础上修改或直接创建? + +用户: "直接创建到 order_db 数据库" + +调用: create_table( + connectionId="58", + databaseName="order_db", + tableName="products", + tableComment="商品表", + columns=[...] // 使用 AI 生成的 columns +) +回复: 已成功创建表 products (商品表) +``` + +### 修改表结构(alter_table) + +如果用户需要修改已有表的结构: + +``` +用户请求: "给 users 表加一个 phone 字段" + ↓ +1. 确认表结构和要修改的内容 + ↓ +2. 构建 operations 数组(支持多种变更类型) + ↓ +3. 调用 alter_table(connectionId="xx", databaseName="xx", tableName="xx", operations=[...]) + ↓ +4. 确认修改成功 +``` + +**可用的 operations 类型**: +- `ADD_COLUMN`:添加字段 +- `DROP_COLUMN`:删除字段 +- `RENAME_COLUMN`:重命名字段 +- `ALTER_COLUMN_TYPE`:修改字段类型 +- `SET_NOT_NULL`:设置为非空 +- `DROP_NOT_NULL`:取消非空约束 +- `SET_DEFAULT`:设置默认值 +- `DROP_DEFAULT`:删除默认值 + +### 注意事项 +- `requirement` 参数应尽可能详细,包含业务场景和字段需求 +- AI 生成的表结构可能需要用户调整(如字段长度、类型) +- 创建表前需要确认 `connectionId` 和 `databaseName` +- 常用字段类型:`VARCHAR`, `INTEGER`, `SERIAL`(自增主键), `DECIMAL`, `TIMESTAMP`, `TEXT`, `BOOLEAN` +- 如果用户描述模糊,先引导用户补充细节 + +--- + +## 场景 6:数据导入(Excel 到数据库) + +当用户需要从 Excel 文件导入数据到数据库时,使用此流程。 + +### 工作流程 + +``` +用户请求: "帮我导入这个 Excel 文件" / "把表格数据导入数据库" + ↓ +1. 用户将 Excel 文件转为 base64 编码 + ↓ +2. 调用 preview_import_data(connectionId="xx", file_base64="...", file_name="data.xlsx", target="test") + ↓ +3. 展示 AI 识别的表结构和数据预览 + ↓ +4. ⚠️ 安全提醒:此操作可能涉及数据安全风险。 + 说明:导入的数据将写入数据库,请确认数据来源合法合规,不包含敏感信息、政治内容或违规内容。 + 请确认是否继续? + ↓ +5. 用户确认无误后,调用 confirm_import_data(connectionId="xx", data={...}, target="test") + ↓ +6. 确认导入成功 +``` + +### 注意事项 +- 文件大小限制:< 500KB +- 支持格式:.xlsx / .xls +- 导入前默认使用 `test` 环境(安全做法) +- 如果用户要导入到正式环境,必须二次确认 +- base64 编码的文件内容需要提供文件名 + +--- + +## 场景 7:API 密钥管理 + +当用户需要管理 API 密钥时,使用此流程。 + +### 7.1 查看密钥列表 + +``` +调用: list_api_keys() +返回: { + "total": 6, + "rows": [ + {"id": "7", "apiKeyName": "AWINBEXT", "apiKey": "Lb8Lg...", "status": 0, "expireTime": "2027-06-06..."}, + ... + ] +} +``` + +### 7.2 创建新密钥 + +``` +调用: create_api_key(apiKeyName="新密钥名称") +返回: 包含新创建的密钥信息 +``` + +### 7.3 启用/禁用密钥 + +``` +调用: toggle_api_key_status(id="7", status=0) // 0=启用, 1=禁用 +``` + +### 7.4 删除密钥 + +``` +⚠️ 删除前确认: +⚠️ 安全提醒:此操作存在删除风险。 +说明:API 密钥删除后不可恢复,依赖该密钥的服务将失效。 +请确认是否继续? + +回复: 确认要删除 API 密钥 "AWINBEXT" 吗?此操作不可恢复。 + +调用: delete_api_key(id="7") +``` + +### 7.5 查看密钥权限 + +``` +调用: get_api_key_permissions(apiKeyId="7") +``` + +### 7.6 批量授权 + +``` +调用: grant_api_key_permissions( + apiKeyId="7", + batchDatas=[ + { + "connectionId": "58", + "permissionLevel": "connection", + "permissionType": "read,write" + }, + { + "connectionId": "58", + "permissionLevel": "database", + "databaseName": "order_db", + "permissionType": "read" + } + ] +) +``` + +权限级别说明: +- `connection`:数据源级别权限 +- `database`:数据库级别权限 +- `table`:表级别权限 + +--- + +## 场景 8:技能与工具管理 + +当用户需要创建和管理自定义技能时,使用此流程。 + +### 8.1 查看数据源关联的技能 + +``` +调用: get_skill_by_datasource(datasourceId="58") +``` + +### 8.2 创建技能 + +``` +调用: create_skill(datasourceId="58", name="订单查询技能", description="用于订单数据的常用查询") +``` + +### 8.3 查看技能下的工具 + +``` +调用: get_skill_tools(skillId="xx") +``` + +### 8.4 将 SQL 创建为可复用工具 + +``` +调用: create_sql_tool( + skillId="xx", + tableIds=["5"], + suggestions=[{ + "name": "查询活跃用户", + "businessDescription": "查询所有状态为活跃的用户", + "sqlTemplate": "SELECT * FROM users WHERE status = #{status}", + "sqlParams": {"status": {"type": "string", "default": "active"}}, + "resultType": "list", + "businessScenario": "用于查看当前活跃用户列表" + }] +) +``` + +### 8.5 删除技能工具 + +``` +⚠️ 删除前确认: +⚠️ 安全提醒:此操作存在删除风险。 +说明:技能工具删除后不可恢复。 +请确认是否继续? + +调用: delete_skill_tool(skillToolId="xx") +``` + +### 8.6 更新技能配置 + +``` +调用: update_skill_config( + datasourceId="58", + configTemplate='{"mcpServer": "..."}' // JSON 字符串 +) +``` + +--- + +## 场景 9:表订阅管理 + +``` +用户请求: "订阅 orders 表" / "取消订阅 users 表" + +调用: toggle_table_subscription( + configId="数据库配置ID", + tableName="orders", + isSubscribe=true // true=订阅, false=取消订阅 +) +``` + +--- + +## 最佳实践 + +### 1. 安全第一 + +#### 通用安全确认规则 + +- 执行任何工具前,评估是否存在删除、泄密、政治敏感、似黄等风险 +- 存在风险时,**必须询问用户确认**,说明后果,不得擅作主张 +- 用户未明确确认前,不得执行 + +- 执行任何写操作(INSERT/UPDATE/DELETE)前,先确认环境(prod vs test) +- 删除操作必须向用户展示将要删除的数据并二次确认 +- 危险操作(DELETE/DROP/TRUNCATE/UPDATE 影响多行)前必须明确告知用户风险 +- 数据导入时默认使用 `test` 环境 + +### 2. 分步引导 + +- 新手可能不知道 `datasourceId`、`tableId` 等参数,先通过列表工具引导获取 +- 复杂操作分步执行,每步确认后继续 +- 如果用户请求不完整(如未指定数据源),先引导补充信息 + +### 3. 数据展示 + +- 表格数据使用 Markdown 表格格式 +- 长文本截断显示(最多 100 字符) +- 数字格式化(千位分隔符、货币符号) +- 时间格式化为可读格式 +- 展示数据时包含字段名和类型 + +### 4. 错误处理 + +- API 错误:展示错误信息,建议重试或检查参数 +- SQL 错误:展示 SQL 错误位置,建议修正 +- 连接错误:检查数据源状态,建议启用或重新配置 +- "登录过期":提示用户检查 `API_KEY` 环境变量 + +### 5. 沟通方式 + +- 向小白用户解释时,避免使用技术术语(如 JSON Schema、SERIAL 等),用通俗语言 +- 操作完成后告知结果(成功/失败/影响行数) +- 如果用户操作成功,给出明确的反馈信息 + +--- + +## 常用参数参考 + +### 数据源类型 +- `builtin` - 内置 PostgreSQL +- `external` - 外部数据库(MySQL/PostgreSQL/Oracle/SQL Server/达梦等) + +### 数据源状态 +- `0` - 运行中 +- `1` - 已停止 + +### 环境 +- `prod` - 生产环境(默认) +- `test` - 测试环境 + +### 常用字段类型 +| 类型 | 用途 | 示例 | +|------|------|------| +| `SERIAL` | 自增主键 | `id SERIAL PRIMARY KEY` | +| `VARCHAR(n)` | 短文本 | `username VARCHAR(50)` | +| `TEXT` | 长文本 | `description TEXT` | +| `INTEGER` | 整数 | `stock INTEGER` | +| `DECIMAL(m,n)` | 精确小数 | `price DECIMAL(10,2)` | +| `TIMESTAMP` | 时间戳 | `created_at TIMESTAMP` | +| `BOOLEAN` | 布尔值 | `is_active BOOLEAN` | + +### 表结构变更操作类型(alter_table) +| 类型 | 用途 | +|------|------| +| `ADD_COLUMN` | 添加字段 | +| `DROP_COLUMN` | 删除字段 | +| `RENAME_COLUMN` | 重命名字段 | +| `ALTER_COLUMN_TYPE` | 修改字段类型 | +| `SET_NOT_NULL` | 设置为非空 | +| `DROP_NOT_NULL` | 取消非空约束 | +| `SET_DEFAULT` | 设置默认值 | +| `DROP_DEFAULT` | 删除默认值 | + +### 权限级别(API 密钥授权) +| 级别 | 范围 | +|------|------| +| `connection` | 整个数据源 | +| `database` | 指定数据库 | +| `table` | 指定表 | + +--- + +## 快速开始 + +如果用户说"帮我查一下数据库",按以下步骤操作: + +1. 调用 `list_datasources()` 获取数据源列表 +2. 展示列表,让用户选择或默认第一个运行中的数据源 +3. 调用 `get_datasource_detail(datasourceId="xx")` 获取数据库和表信息 +4. 引导用户选择要操作的表 +5. 根据用户意图调用相应的工具: + - 想看数据 → `query_table_data` + - 想执行 SQL → `execute_sql` + - 想新增数据 → `insert_table_row` + - 想修改数据 → `update_table_row` + - 想删除数据 → `delete_table_rows`(需确认) + - 想导出 Excel → `export_table_excel` + - 想创建表 → `generate_table_by_description` + `create_table` + - 想导入 Excel → `preview_import_data` + `confirm_import_data` diff --git a/.kilo/skills/mcp-tool-testing.zip b/.kilo/skills/mcp-tool-testing.zip new file mode 100644 index 0000000..5303d59 Binary files /dev/null and b/.kilo/skills/mcp-tool-testing.zip differ diff --git a/.kilo/skills/mcp-tool-testing/SKILL.md b/.kilo/skills/mcp-tool-testing/SKILL.md new file mode 100644 index 0000000..49ef116 --- /dev/null +++ b/.kilo/skills/mcp-tool-testing/SKILL.md @@ -0,0 +1,356 @@ +--- +name: mcp-tool-testing +description: MCP 工具通用测试技能。自动发现、执行和组合测试任何 MCP 服务器中的工具,支持场景自动创造和风险前置询问。 +version: 0.1.0 +--- + +# MCP Tool Testing Skill + +这是一个通用的 MCP(Model Context Protocol)工具测试技能。适用于任何 MCP 环境,无论是自研的还是第三方的。 + +## 核心原则 + +1. **通用性**:不假设任何具体的服务器名称、工具名称或项目结构 +2. **自动发现**:通过 MCP 协议本身获取当前环境中可用的所有工具 +3. **智能执行**:能判断的入参自动执行,不能判断的询问用户 +4. **场景自动创造**:根据工具自动匹配并创造测试场景,无需用户指定 +5. **风险前置**:有风险的操作必须询问用户确认,绝不擅作主张 +6. **结果汇总**:最终只返回执行工具的入参、出参和理解 + +## 执行流程 + +### 第一步:发现可用工具 + +通过 MCP 的 `tools/list` 方法获取当前环境中所有可用的工具。 + +**获取每个工具的信息:** +- 工具名称(name) +- 工具描述(description) +- 入参 schema(inputSchema): + - 参数名称 + - 参数类型(string, integer, boolean, object, array, enum 等) + - 是否必填(required) + - 默认值(default) + - 枚举值(enum) + - 参数描述(description) + +### 第二步:分析工具并分类 + +根据工具名称和描述,自动对工具进行分类: + +1. **查询类**:list_xxx, get_xxx, query_xxx, search_xxx, find_xxx +2. **创建类**:create_xxx, add_xxx, insert_xxx, new_xxx +3. **更新类**:update_xxx, edit_xxx, modify_xxx, alter_xxx +4. **删除类**:delete_xxx, remove_xxx, drop_xxx +5. **执行类**:execute_xxx, run_xxx, call_xxx, invoke_xxx +6. **状态类**:toggle_xxx, enable_xxx, disable_xxx, start_xxx, stop_xxx +7. **其他**:无法归类的工具 + +### 第三步:执行单个工具 + +对每个工具,按以下策略执行: + +#### 入参判断逻辑 + +``` +对于每个必填参数(required 中的参数): + 1. schema 有 default 值 → 使用该默认值 + 2. schema 有 enum 且只有一个值 → 使用该值 + 3. schema 有 enum 且有多个值 → 使用第一个,或询问用户 + 4. 参数名是常见的通用类型: + - pageNum, page, offset → 使用 1 + - pageSize, limit, size → 使用 10 + - target, environment → 使用 "prod" 或第一个 enum 值 + - status, enabled → 使用 0 或 true + - name, title → 使用测试名称如 "test_item" + - id, xxxId → 需要从其他工具查询获取,或询问用户 + 5. 可以从之前执行的工具结果中获取 → 使用前一个工具的输出 + 6. 以上都不满足 → 询问用户 + +对于非必填参数: + - 一般不传,使用服务器默认值 + - 如果需要测试特定功能,可以传一个合理值 +``` + +#### 询问用户的标准 + +当遇到以下情况时必须询问: + +**1. 参数不明确** + - 参数是必填的,且值完全不明确 + - 例如:某个特定的业务 ID、密钥、路径等 + +**2. 参数有多个合理选项且无法自动判断** + - 例如:enum 有 ["mysql", "postgresql", "oracle"],不知道测试用哪个 + +**3. 参数涉及敏感信息** + - 例如:password, token, apiKey, secret 等 + +**4. 高风险操作必须询问(绝不擅作主张)** + + 以下类型的工具/操作在执行前必须先询问用户确认: + + - **删除类**:delete_xxx, remove_xxx, drop_xxx, destroy_xxx + - 风险:数据丢失,不可恢复 + - 询问内容:确认是否要执行删除操作,指定删除目标或使用测试数据 + + - **泄密风险类**:export_xxx, dump_xxx, download_xxx, get_all_xxx(大量数据) + - 风险:可能导出敏感业务数据、用户信息、密钥等 + - 询问内容:确认是否要导出数据,是否使用脱敏测试数据 + + - **政治敏感类**:涉及政府、政策、领导人等相关内容的工具 + - 风险:可能触及政治敏感话题 + - 询问内容:确认测试方向和范围 + + - **似黄/色情风险类**:涉及用户生成内容、图片、文本审核等相关工具 + - 风险:可能接触到不当内容 + - 询问内容:确认是否使用安全的测试数据 + + - **生产环境操作类**:影响生产环境数据的工具 + - 风险:可能影响真实业务 + - 询问内容:确认是否在测试环境执行,或使用只读模式 + +**5. 更新/修改类操作需要确认** + - 例如:update_xxx, modify_xxx 可能改变真实数据 + +**高风险操作询问格式:** + +``` +⚠️ 高风险操作提醒 + +工具:[工具名] +风险类型:[删除/泄密/政治敏感/似黄/生产环境影响] +具体风险:[描述可能的风险] + +请选择: +1. 确认执行,使用测试数据 +2. 确认执行,使用真实数据(我知道风险) +3. 跳过此工具 +4. 其他指示 +``` + +**普通询问格式:** + +``` +工具:[工具名] +参数:[参数名](类型:[类型],必填:是/否) +用途:[从 description 中提取] + +请选择: +1. [建议选项1,如果有] +2. [建议选项2,如果有] +3. 提供自定义值 + +或者告诉我使用什么值。 +``` + +#### 执行顺序策略 + +1. **先执行无参数或少参数的查询工具** + - 如 list_xxx、get_all_xxx 等 + - 这些工具通常不需要太多参数,可以直接执行 + - 执行结果可以为后续工具提供 ID 等参数 + +2. **利用查询结果作为后续工具的入参** + - 例如:list_datasources 返回的 id 用于 get_datasource_detail + - 例如:list_tables 返回的 tableId 用于 query_table_data + +3. **最后执行创建/修改/删除类工具** + - 这些通常需要更多上下文参数 + - 删除操作要特别小心,确认是测试数据 + +### 第四步:场景自动创造与搭配测试 + +这是本技能的核心能力:根据当前所有可用工具,自动创造有意义的测试场景,无需用户指定。 + +#### 场景识别规则 + +根据工具的名称、描述和参数,自动匹配以下 8 种场景模式: + +**1. 查询链路场景** + - 匹配规则:存在 list_xxx + get_xxx_detail/xxx_detail + query/get_data 类工具 + - 创造方式:list 获取列表 → 取第一条的 ID → get detail 获取详情 → 用详情中的 ID 查询关联数据 + - 示例:list_datasources → get_datasource_detail(id) → list_tables(datasourceId) → get_table_detail(tableId) + +**2. 完整 CRUD 场景** + - 匹配规则:存在 create_xxx + list/query_xxx + update_xxx + delete_xxx 类工具 + - 创造方式:create 创建测试数据 → list 确认存在 → update 修改 → list 确认修改 → delete 删除 → list 确认删除 + - 示例:create_api_key → list_api_keys → toggle_api_key_status → list_api_keys → delete_api_key → list_api_keys + +**3. 状态变更验证场景** + - 匹配规则:存在 get/list_xxx + toggle/enable/disable/start/stop_xxx 类工具 + - 创造方式:get 初始状态 → toggle 变更状态 → get 验证状态已变更 → toggle 恢复 → get 验证恢复 + - 示例:get_datasource_detail → toggle_datasource_status(status=1) → get_datasource_detail → toggle_datasource_status(status=0) → get_datasource_detail + +**4. 参数传递链路场景** + - 匹配规则:工具 A 的输出字段与工具 B 的输入参数名匹配或语义相关 + - 创造方式:执行 A → 从结果提取字段 → 作为 B 的输入 → 执行 B → 继续传递给 C + - 示例:list_datasources 返回 datasourceId → get_datasource_detail(datasourceId) 返回 connectionId → execute_sql(connectionId, sql) + +**5. 批量操作场景** + - 匹配规则:存在接受 array 类型参数的工具(如批量删除、批量授权) + - 创造方式:list 获取多条记录 → 提取 IDs 数组 → 传递给批量操作工具 → 验证结果 + - 示例:list_api_keys → 提取 ids → grant_api_key_permissions(batchDatas=[{...}]) + +**6. 条件分支场景** + - 匹配规则:工具支持不同枚举参数或可选参数,产生不同行为 + - 创造方式:同一工具传不同参数 → 对比输出差异 + - 示例:query_table_data(tableId, target="prod") vs query_table_data(tableId, target="test") + +**7. 异常处理场景** + - 匹配规则:任何工具 + - 创造方式:传入无效 ID、空参数、错误类型 → 验证错误处理是否合理 + - 示例:get_datasource_detail(datasourceId="invalid") → 验证返回错误信息 + +**8. 跨服务器场景**(当有多个 MCP 服务器时) + - 匹配规则:不同服务器的工具之间存在业务关联 + - 创造方式:服务器A的输出 → 作为服务器B的输入 + - 示例:IoT 获取设备列表 → Dify workflow 处理设备数据 → SQL 执行存储结果 + +#### 场景自动创造流程 + +``` +1. 收集所有工具,建立工具字典 + - key: 工具名 + - value: {description, inputSchema, outputSample, category} + +2. 构建参数依赖图 + - 分析每个工具的输入参数名(如 datasourceId, tableId, apiKeyId) + - 分析每个工具的输出字段(从执行结果中提取) + - 建立 "输出字段 → 输入参数" 的映射关系 + +3. 匹配场景模式 + - 遍历上述 8 种场景规则 + - 对每种规则,检查当前工具集是否满足匹配条件 + - 满足则创造具体场景实例 + +4. 场景优先级排序 + - P0: 查询链路(最基础,最先执行) + - P1: CRUD 完整链路(验证完整生命周期) + - P2: 状态变更验证(验证状态管理) + - P3: 参数传递链路(验证工具间协作) + - P4: 批量操作、条件分支、异常处理 + - P5: 跨服务器场景(最复杂,最后执行) + +5. 执行场景 + - 按优先级依次执行 + - 前一步的输出自动传递给下一步 + - 任何一步失败则标记场景失败,继续下一个场景 + - 场景中包含高风险操作时,执行前询问用户 +``` + +#### 场景执行记录格式 + +```markdown +### 场景:[场景名称] +- **触发规则**:[匹配的场景模式,如 "查询链路场景"] +- **涉及工具**:工具A → 工具B → 工具C +- **执行过程**: + | 步骤 | 工具 | 入参 | 出参摘要 | 状态 | + |------|------|------|---------|------| + | 1 | list_xxx | {} | 返回 5 条记录 | ✅ | + | 2 | get_xxx | {id: "从步骤1获取"} | 返回详情 | ✅ | + | 3 | query_xxx | {xxxId: "从步骤2获取"} | 返回结果 | ✅ | +- **数据流**:步骤1.id → 步骤2.id → 步骤2.connectionId → 步骤3.connectionId +- **场景结果**:✅ 全部成功 / ❌ 步骤N失败(原因:...) +- **理解**:[对这个场景的整体理解,工具间如何协作] +``` + +**场景测试要点:** +- 记录完整的数据流转过程 +- 验证工具之间的兼容性 +- 检查数据格式是否一致 +- 场景创造过程不需要用户干预,自动完成 +- 只在遇到无法判断的参数或高风险操作时才询问用户 + +### 第五步:生成报告 + +最终只返回以下内容: + +```markdown +# MCP 工具测试报告 + +## 工具列表 + +共发现 [N] 个工具: + +| 序号 | 工具名 | 描述 | 必填参数 | 分类 | +|------|--------|------|---------|------| + +## 工具执行结果 + +### 1. [工具名] +- **描述**:[工具描述] +- **入参**:`{JSON}` +- **出参**:`{JSON 摘要}` +- **状态**:✅ 成功 / ❌ 失败 / ⚠️ 跳过(原因:...) +- **理解**:[你对这个工具功能的简要理解] + +### 2. [工具名] +... + +## 场景测试 + +### 场景1:[场景名称] +- **流程**:工具A → 工具B → 工具C +- **数据流**:[描述数据如何在工具间传递] +- **结果**:[最终结果摘要] +- **状态**:✅ 成功 / ❌ 失败 + +## 总结 + +| 指标 | 数量 | +|------|------| +| 发现工具总数 | N | +| 成功执行 | N | +| 执行失败 | N | +| 跳过(需用户提供信息) | N | +| 高风险操作(已询问用户) | N | +| 场景测试数 | N | + +## 待确认事项 + +列出需要用户提供的参数或信息: +1. [工具名] 的 [参数名]:[说明] +2. ... +``` + +## 特殊处理 + +### 动态工具 + +有些服务器启动时从外部加载工具定义(如从 API、配置文件、工作流引擎等): +1. 通过 MCP 协议获取实际可用的工具列表 +2. 对动态获取的工具同样按照上述流程测试 +3. 记录工具的来源信息(如果有) + +### 需要认证/配置的服务器 + +某些 MCP 服务器需要特定的环境变量、API Key、Token 等: +1. 检查服务器是否正常运行 +2. 如果启动失败,记录错误信息并跳过 +3. 如果运行正常但工具调用失败(如 401),询问用户提供认证信息 + +### 失败处理 + +- **网络超时**:记录错误,标记为失败,继续下一个 +- **参数错误**:检查是否需要补充参数,或询问用户 +- **服务器未运行**:记录原因,跳过该服务器的所有工具 +- **工具不存在**:可能工具定义已变更,重新获取工具列表 + +### 输出处理 + +- 输出过长时适当截断(如超过 2000 字符) +- 保留关键信息:状态码、主要数据、错误信息 +- 对于列表类输出,显示前几条和总数 + +## 注意事项 + +1. **保持参数一致性**:同一个 ID 在多个工具中保持相同 +2. **风险前置**:高风险操作必须先询问用户,绝不擅作主张 +3. **记录完整**:每个工具的入参、出参都要记录 +4. **及时询问**:遇到不明确的参数不要猜测,询问用户 +5. **场景优先**:优先测试有意义的场景组合,而不是孤立地测试每个工具 +6. **自动创造**:场景不需要用户指定,根据工具自动匹配和创造 +7. **最终输出**:只返回入参、出参和理解,不需要冗长的过程描述 +8. **五类风险**:删除、泄密、政治、似黄、生产环境影响——必须询问用户 diff --git a/lzwcai_mcp_agile_db/README.md b/lzwcai_mcp_agile_db/README.md index 383820b..8ae50f6 100644 --- a/lzwcai_mcp_agile_db/README.md +++ b/lzwcai_mcp_agile_db/README.md @@ -6,8 +6,8 @@ | 变量名 | 必填 | 说明 | |--------|------|------| -| `AGILE_DB_API_KEY` | 是 | 数据库管理平台的 API 密钥 | -| `AGILE_DB_BASE_URL` | 否 | 数据库管理平台后端地址(默认 `http://localhost:8080`) | +| `API_KEY` | 是 | 数据库管理平台的 API 密钥(格式: `Bearer `) | +| `backendBaseUrl` | 否 | 数据库管理平台后端地址(默认 `http://lzwcai-demp-corp-manager:8086`) | ## 安装 @@ -19,8 +19,8 @@ pip install -e . ```bash # 设置环境变量 -export AGILE_DB_API_KEY="your-api-key" -export AGILE_DB_BASE_URL="http://localhost:8080" # 可选 +export API_KEY="Bearer your-token" +export backendBaseUrl="https://dempdemo.lzwcai.com" # 可选 # 运行 MCP Server lzwcai-mcp-agile-db diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/main.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/main.py deleted file mode 100644 index 194ed3d..0000000 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from lzwcai-mcp-agile-db!") - - -if __name__ == "__main__": - main() diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/pyproject.toml b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/pyproject.toml deleted file mode 100644 index 5e91253..0000000 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/pyproject.toml +++ /dev/null @@ -1,7 +0,0 @@ -[project] -name = "lzwcai-mcp-agile-db" -version = "0.1.0" -description = "Add your description here" -readme = "README.md" -requires-python = ">=3.12" -dependencies = [] diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/_base.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/_base.py index 52ccf5e..7b3468c 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/_base.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/_base.py @@ -26,7 +26,7 @@ def register_tool(name: str): input_schema = {...} async def execute(self, args): - return await self.client.get("/api/...", params=args) + return await self.client.get("/datasource/...", params=args) Args: name: 工具名称(唯一标识) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py index c309701..16b29c5 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py @@ -22,7 +22,7 @@ class ListApiKeysTool(ToolDef): async def execute(self, args: dict) -> dict: args = dict(args) params = {k: v for k, v in args.items() if v is not None} - return self.client.get("/api/datasource/api_key/list", params=params) + return await self.client.get("/datasource/api_key/list", params=params) @register_tool("create_api_key") @@ -38,7 +38,7 @@ class CreateApiKeyTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.post("/api/datasource/api_key", json_data=args) + return await self.client.post("/datasource/api_key", json_data=args) @register_tool("toggle_api_key_status") @@ -55,7 +55,7 @@ class ToggleApiKeyStatusTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.put("/api/datasource/api_key", json_data=args) + return await self.client.put("/datasource/api_key", json_data=args) @register_tool("delete_api_key") @@ -71,7 +71,7 @@ class DeleteApiKeyTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.delete(f"/api/datasource/api_key/{args['id']}") + return await self.client.delete(f"/datasource/api_key/{args['id']}") @register_tool("get_api_key_permissions") @@ -87,7 +87,7 @@ class GetApiKeyPermissionsTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.get(f"/api/datasource/api_key/permission/{args['apiKeyId']}") + return await self.client.get(f"/datasource/api_key/permission/{args['apiKeyId']}") @register_tool("grant_api_key_permissions") @@ -118,4 +118,4 @@ class GrantApiKeyPermissionsTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.post("/api/datasource/api_key/permission/grant_batch", json_data=args) + return await self.client.post("/datasource/api_key/permission/grant_batch", json_data=args) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/data_import.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/data_import.py index 9b4ef0d..26c2478 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/data_import.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/data_import.py @@ -38,8 +38,8 @@ class PreviewImportDataTool(ToolDef): "file": (file_name, io.BytesIO(file_content), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), } - return self.client.upload( - f"/api/datasource/connection/{connection_id}/import_document/preview", + return await self.client.upload( + f"/datasource/connection/{connection_id}/import_document/preview", files=files, params={"target": target}, ) @@ -65,8 +65,8 @@ class ConfirmImportDataTool(ToolDef): target = args.pop("target", "test") data = args.pop("data") - return self.client.post( - f"/api/datasource/connection/{connection_id}/import_document/confirm", + return await self.client.post( + f"/datasource/connection/{connection_id}/import_document/confirm", json_data=data, params={"target": target}, ) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/database_tables.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/database_tables.py index 274838c..e524b44 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/database_tables.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/database_tables.py @@ -22,7 +22,7 @@ class ListDatabasesTool(ToolDef): async def execute(self, args: dict) -> dict: params = {k: v for k, v in args.items() if v is not None} - return self.client.get("/api/datasource/config/list", params=params) + return await self.client.get("/datasource/config/list", params=params) @register_tool("list_tables") @@ -42,7 +42,7 @@ class ListTablesTool(ToolDef): async def execute(self, args: dict) -> dict: params = {k: v for k, v in args.items() if v is not None} - return self.client.get("/api/datasource/table/list", params=params) + return await self.client.get("/datasource/table/list", params=params) @register_tool("get_table_detail") @@ -59,7 +59,7 @@ class GetTableDetailTool(ToolDef): async def execute(self, args: dict) -> dict: table_id = args["tableId"] - return self.client.get(f"/api/datasource/table/{table_id}/detail") + return await self.client.get(f"/datasource/table/{table_id}/detail") @register_tool("create_table") @@ -98,7 +98,7 @@ class CreateTableTool(ToolDef): async def execute(self, args: dict) -> dict: args = dict(args) connection_id = args.pop("connectionId") - return self.client.post(f"/api/datasource/connection/{connection_id}/create_table", json_data=args) + return await self.client.post(f"/datasource/connection/{connection_id}/create_table", json_data=args) @register_tool("alter_table") @@ -131,7 +131,7 @@ class AlterTableTool(ToolDef): async def execute(self, args: dict) -> dict: args = dict(args) connection_id = args.pop("connectionId") - return self.client.put(f"/api/datasource/connection/{connection_id}/alter_table", json_data=args) + return await self.client.put(f"/datasource/connection/{connection_id}/alter_table", json_data=args) @register_tool("generate_table_by_description") @@ -148,4 +148,4 @@ class GenerateTableByDescriptionTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.post("/api/datasource/connection/generate_table", json_data=args) + return await self.client.post("/datasource/connection/generate_table", json_data=args) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/datasources.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/datasources.py index 9cb5e66..c38ea17 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/datasources.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/datasources.py @@ -27,7 +27,7 @@ class ListDatasourcesTool(ToolDef): async def execute(self, args: dict) -> dict: params = {k: v for k, v in args.items() if v is not None} - return self.client.get("/api/datasource/connection/list", params=params) + return await self.client.get("/datasource/connection/list", params=params) @register_tool("get_datasource_detail") @@ -47,17 +47,17 @@ class GetDatasourceDetailTool(ToolDef): result = {} # 获取基本信息 try: - result["detail"] = self.client.get(f"/api/datasource/connection/{ds_id}") + result["detail"] = await self.client.get(f"/datasource/connection/{ds_id}") except Exception as e: result["detail"] = {"error": str(e)} # 获取配置 try: - result["config"] = self.client.get(f"/api/datasource/config/{ds_id}") + result["config"] = await self.client.get(f"/datasource/config/{ds_id}") except Exception as e: result["config"] = {"error": str(e)} # 获取实时结构 try: - result["structure"] = self.client.get(f"/api/datasource/connection/realtime/structure/{ds_id}") + result["structure"] = await self.client.get(f"/datasource/connection/realtime/structure/{ds_id}") except Exception as e: result["structure"] = {"error": str(e)} return result @@ -100,12 +100,12 @@ class CreateDatasourceTool(ToolDef): "password": args.get("password"), "connectionType": args.get("connectionType", "user_password"), } - test_result = self.client.post("/api/datasource/connection/test", json_data=test_data) + test_result = await self.client.post("/datasource/connection/test", json_data=test_data) if test_result.get("code") != 200: return {"success": False, "error": f"连接测试失败: {test_result.get('msg', '未知错误')}"} # 创建数据源 - return self.client.post("/api/datasource/connection", json_data=args) + return await self.client.post("/datasource/connection", json_data=args) @register_tool("update_datasource") @@ -128,7 +128,7 @@ class UpdateDatasourceTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.put("/api/datasource/connection", json_data=args) + return await self.client.put("/datasource/connection", json_data=args) @register_tool("toggle_datasource_status") @@ -145,7 +145,7 @@ class ToggleDatasourceStatusTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.put("/api/datasource/connection/changeStatus", json_data=args) + return await self.client.put("/datasource/connection/changeStatus", json_data=args) @register_tool("delete_datasource") @@ -164,10 +164,10 @@ class DeleteDatasourceTool(ToolDef): ds_id = args["id"] # 先尝试停用(仅忽略已停用等预期错误) try: - self.client.put("/api/datasource/connection/changeStatus", json_data={"id": ds_id, "status": 1}) + await self.client.put("/datasource/connection/changeStatus", json_data={"id": ds_id, "status": 1}) except Exception as e: # 记录日志但继续删除 logger.debug(f"停用数据源失败(可能已停用): {e}") # 删除数据源 - return self.client.delete(f"/api/datasource/connection/{ds_id}") + return await self.client.delete(f"/datasource/connection/{ds_id}") diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/skills.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/skills.py index 50cbe5b..1a3af12 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/skills.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/skills.py @@ -20,7 +20,7 @@ class GetSkillByDatasourceTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.get(f"/api/datasource/skill/getByDatasource/{args['datasourceId']}") + return await self.client.get(f"/datasource/skill/getByDatasource/{args['datasourceId']}") @register_tool("get_skill_tools") @@ -36,7 +36,7 @@ class GetSkillToolsTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.get(f"/api/datasource/skill/getBySkillId/{args['skillId']}") + return await self.client.get(f"/datasource/skill/getBySkillId/{args['skillId']}") @register_tool("create_skill") @@ -54,7 +54,7 @@ class CreateSkillTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.post("/api/datasource/skill/createOrGet", json_data=args) + return await self.client.post("/datasource/skill/createOrGet", json_data=args) @register_tool("create_sql_tool") @@ -97,7 +97,7 @@ class CreateSqlToolTool(ToolDef): for suggestion in args["suggestions"]: if "sqlParams" in suggestion and isinstance(suggestion["sqlParams"], dict): suggestion["sqlParams"] = json.dumps(suggestion["sqlParams"]) - return self.client.post("/api/datasource/skill/confirmTools", json_data=args) + return await self.client.post("/datasource/skill/confirmTools", json_data=args) @register_tool("delete_skill_tool") @@ -113,7 +113,7 @@ class DeleteSkillToolTool(ToolDef): } async def execute(self, args: dict) -> dict: - return self.client.delete(f"/api/datasource/skill/tskilltool/{args['skillToolId']}") + return await self.client.delete(f"/datasource/skill/tskilltool/{args['skillToolId']}") @register_tool("update_skill_config") @@ -134,4 +134,4 @@ class UpdateSkillConfigTool(ToolDef): # 如果 configTemplate 是 dict,转为 JSON 字符串 if "configTemplate" in args and isinstance(args["configTemplate"], dict): args["configTemplate"] = json.dumps(args["configTemplate"]) - return self.client.post("/api/datasource/skill/updateOrGet", json_data=args) + return await self.client.post("/datasource/skill/updateOrGet", json_data=args) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/sql_execution.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/sql_execution.py index e041731..07ddf92 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/sql_execution.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/sql_execution.py @@ -13,13 +13,26 @@ class ExecuteSqlTool(ToolDef): "type": "object", "properties": { "datasourceId": {"type": "string", "description": "数据源 ID"}, - "executableSql": {"type": "string", "description": "SQL 语句"}, + "sql": {"type": "string", "description": "SQL 语句"}, + "target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"}, "sqlTemplate": {"type": "string", "description": "SQL 模板(可选)"}, "businessName": {"type": "string", "description": "业务名称(可选)"}, - "parameters": {"type": "object", "description": "参数定义(可选)"}, + "params": {"type": "object", "description": "SQL 参数对象(可选)"}, }, - "required": ["datasourceId", "executableSql"], + "required": ["datasourceId", "sql"], } async def execute(self, args: dict) -> dict: - return self.client.post("/api/datasource/sqlExecutionLog/testSqlWithSchema", json_data=args) + # 映射参数名为后端 API 期望的格式 + body = {} + if "datasourceId" in args: + body["datasourceId"] = args["datasourceId"] + if "sql" in args: + body["executableSql"] = args["sql"] + if "sqlTemplate" in args: + body["sqlTemplate"] = args["sqlTemplate"] + if "businessName" in args: + body["businessName"] = args["businessName"] + if "params" in args: + body["parameters"] = args["params"] + return await self.client.post("/datasource/sqlExecutionLog/testSqlWithSchema", json_data=body) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/subscriptions.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/subscriptions.py index 33394ad..6080cf0 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/subscriptions.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/subscriptions.py @@ -12,12 +12,18 @@ class ToggleTableSubscriptionTool(ToolDef): input_schema = { "type": "object", "properties": { - "configId": {"type": "string", "description": "数据库配置 ID"}, - "tableName": {"type": "string", "description": "表名"}, - "isSubscribe": {"type": "boolean", "description": "true=订阅, false=取消订阅"}, + "tableId": {"type": "string", "description": "表 ID"}, + "datasourceId": {"type": "string", "description": "数据源 ID"}, + "subscribe": {"type": "boolean", "description": "true=订阅, false=取消订阅"}, }, - "required": ["configId", "tableName", "isSubscribe"], + "required": ["tableId", "datasourceId", "subscribe"], } async def execute(self, args: dict) -> dict: - return self.client.post("/api/datasource/subscription/toggle", json_data=args) + # 映射参数名为后端 API 期望的格式 + body = { + "tableId": args["tableId"], + "datasourceId": args["datasourceId"], + "subscribe": args["subscribe"], + } + return await self.client.post("/datasource/subscription/toggle", json_data=body) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/table_data.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/table_data.py index 55baed8..67bf8c0 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/table_data.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/table_data.py @@ -26,7 +26,7 @@ class QueryTableDataTool(ToolDef): args = dict(args) table_id = args.pop("tableId") params = {k: v for k, v in args.items() if v is not None} - return self.client.get(f"/api/datasource/connection/builtin/table/{table_id}", params=params) + return await self.client.get(f"/datasource/connection/builtin/table/{table_id}", params=params) @register_tool("insert_table_row") @@ -49,7 +49,7 @@ class InsertTableRowTool(ToolDef): target = args.pop("target", "prod") data = args.pop("data", {}) params = {"target": target} if target else {} - return self.client.post(f"/api/datasource/connection/builtin/table/{table_id}/rows", json_data=data, params=params) + return await self.client.post(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=data, params=params) @register_tool("update_table_row") @@ -75,7 +75,7 @@ class UpdateTableRowTool(ToolDef): data = args.pop("data", {}) params = {"target": target} if target else {} body = {"primaryKey": primary_key, "data": data} - return self.client.put(f"/api/datasource/connection/builtin/table/{table_id}/rows", json_data=body, params=params) + return await self.client.put(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=body, params=params) @register_tool("delete_table_rows") @@ -103,7 +103,7 @@ class DeleteTableRowsTool(ToolDef): primary_keys = args.pop("primaryKeys") params = {"target": target} if target else {} body = {"primaryKeys": primary_keys} - return self.client.delete(f"/api/datasource/connection/builtin/table/{table_id}/rows", json_data=body, params=params) + return await self.client.delete(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=body, params=params) @register_tool("export_table_excel") @@ -124,7 +124,7 @@ class ExportTableExcelTool(ToolDef): table_id = args.pop("tableId") target = args.pop("target", "prod") params = {"target": target} if target else {} - result = self.client.get(f"/api/datasource/connection/builtin/table/{table_id}/export/excel", params=params) + result = await self.client.get(f"/datasource/connection/builtin/table/{table_id}/export/excel", params=params) # 处理二进制响应 if result.get("raw"): diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/api_client.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/api_client.py index 744622f..c0dd25a 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/api_client.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/api_client.py @@ -30,8 +30,8 @@ class AgileDBAPIClient: 初始化 API 客户端 Args: - base_url: API 基础 URL(默认从环境变量 AGILE_DB_BASE_URL 读取) - api_key: API 密钥(默认从环境变量 AGILE_DB_API_KEY 读取) + base_url: API 基础 URL(默认从环境变量 backendBaseUrl 读取) + api_key: API 密钥(默认从环境变量 API_KEY 读取) default_timeout: 请求超时时间(秒),默认 30 秒 """ if base_url is None: @@ -43,15 +43,15 @@ class AgileDBAPIClient: self.base_url = base_url.rstrip('/') self.api_key = api_key self.default_timeout = default_timeout - self._client: Optional[httpx.Client] = None + self._client: Optional[httpx.AsyncClient] = None logger.info(f"[客户端初始化] base_url={self.base_url}") @property - def client(self) -> httpx.Client: + def client(self) -> httpx.AsyncClient: """懒加载 HTTP 客户端""" if self._client is None: - self._client = httpx.Client(timeout=self.default_timeout) + self._client = httpx.AsyncClient(timeout=self.default_timeout) return self._client def _get_headers(self, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]: @@ -64,10 +64,14 @@ class AgileDBAPIClient: return headers def _build_url(self, path: str) -> str: - """构建完整 URL""" + """构建完整 URL,自动去掉路径中多余的 /api 前缀""" if path.startswith('http://') or path.startswith('https://'): return path - return f"{self.base_url}{path}" + # 去掉 /api 前缀,因为 base_url 已经包含完整地址 + clean_path = path + if clean_path.startswith('/api'): + clean_path = clean_path[4:] + return f"{self.base_url}{clean_path}" def _handle_response(self, response: httpx.Response, url: str) -> Dict[str, Any]: """统一处理 API 响应""" @@ -93,12 +97,12 @@ class AgileDBAPIClient: return data - def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """发送 GET 请求""" url = self._build_url(path) try: logger.info(f"[API请求] GET {url}") - response = self.client.get(url, headers=self._get_headers(), params=params) + response = await self.client.get(url, headers=self._get_headers(), params=params) return self._handle_response(response, url) except httpx.TimeoutException: raise Exception(f"API 请求超时: {url}") @@ -107,13 +111,13 @@ class AgileDBAPIClient: except httpx.RequestError as e: raise Exception(f"API 请求异常: {url}, 错误: {str(e)}") - def post(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + async def post(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """发送 POST 请求""" url = self._build_url(path) try: logger.info(f"[API请求] POST {url}") headers = self._get_headers({'Content-Type': 'application/json'}) - response = self.client.post(url, headers=headers, json=json_data, params=params) + response = await self.client.post(url, headers=headers, json=json_data, params=params) return self._handle_response(response, url) except httpx.TimeoutException: raise Exception(f"API 请求超时: {url}") @@ -122,13 +126,13 @@ class AgileDBAPIClient: except httpx.RequestError as e: raise Exception(f"API 请求异常: {url}, 错误: {str(e)}") - def put(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + async def put(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """发送 PUT 请求""" url = self._build_url(path) try: logger.info(f"[API请求] PUT {url}") headers = self._get_headers({'Content-Type': 'application/json'}) - response = self.client.put(url, headers=headers, json=json_data, params=params) + response = await self.client.put(url, headers=headers, json=json_data, params=params) return self._handle_response(response, url) except httpx.TimeoutException: raise Exception(f"API 请求超时: {url}") @@ -137,7 +141,7 @@ class AgileDBAPIClient: except httpx.RequestError as e: raise Exception(f"API 请求异常: {url}, 错误: {str(e)}") - def delete(self, path: str, params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + async def delete(self, path: str, params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """发送 DELETE 请求""" url = self._build_url(path) try: @@ -145,7 +149,7 @@ class AgileDBAPIClient: headers = self._get_headers() if json_data is not None: headers['Content-Type'] = 'application/json' - response = self.client.delete(url, headers=headers, params=params, json=json_data) + response = await self.client.delete(url, headers=headers, params=params, json=json_data) return self._handle_response(response, url) except httpx.TimeoutException: raise Exception(f"API 请求超时: {url}") @@ -154,14 +158,14 @@ class AgileDBAPIClient: except httpx.RequestError as e: raise Exception(f"API 请求异常: {url}, 错误: {str(e)}") - def upload(self, path: str, files: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + async def upload(self, path: str, files: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """发送文件上传请求(multipart/form-data)""" url = self._build_url(path) try: logger.info(f"[API请求] UPLOAD {url}") # 文件上传不需要 Content-Type,httpx 会自动设置 multipart/form-data headers = self._get_headers() - response = self.client.post(url, headers=headers, files=files, params=params) + response = await self.client.post(url, headers=headers, files=files, params=params) return self._handle_response(response, url) except httpx.TimeoutException: raise Exception(f"API 请求超时: {url}") @@ -170,17 +174,17 @@ class AgileDBAPIClient: except httpx.RequestError as e: raise Exception(f"API 请求异常: {url}, 错误: {str(e)}") - def close(self): + async def close(self): """关闭 HTTP 客户端""" if self._client is not None: - self._client.close() + await self._client.aclose() self._client = None - def __enter__(self): + async def __aenter__(self): return self - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() return False diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/env_config.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/env_config.py index 6de69e0..6e19b1d 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/env_config.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/env_config.py @@ -6,7 +6,7 @@ from typing import Optional def get_api_key(default: Optional[str] = None) -> str: """ - 获取数据库管理平台 API 密钥 + 获取 API 密钥 Args: default: 默认值(可选) @@ -15,25 +15,25 @@ def get_api_key(default: Optional[str] = None) -> str: str: API 密钥 Raises: - ValueError: 当 AGILE_DB_API_KEY 未设置且无默认值时 + ValueError: 当 API_KEY 未设置且无默认值时 """ - value = os.environ.get("AGILE_DB_API_KEY", default or "") + value = os.environ.get("API_KEY", default or "") if not value: - raise ValueError("环境变量 AGILE_DB_API_KEY 未设置") + raise ValueError("环境变量 API_KEY 未设置") return value -def get_base_url(default: str = "http://localhost:8080") -> str: +def get_base_url(default: str = "http://lzwcai-demp-corp-manager:8086") -> str: """ - 获取数据库管理平台后端地址 + 获取后端服务地址 Args: - default: 默认值(默认 http://localhost:8080) + default: 默认值(默认 http://lzwcai-demp-corp-manager:8086) Returns: str: 后端 API 基础 URL """ - return os.environ.get("AGILE_DB_BASE_URL", default) + return os.environ.get("backendBaseUrl", default) def get_env_config() -> dict: diff --git a/lzwcai_mcp_agile_db/main.py b/lzwcai_mcp_agile_db/main.py index f53b660..4fea9dd 100644 --- a/lzwcai_mcp_agile_db/main.py +++ b/lzwcai_mcp_agile_db/main.py @@ -7,6 +7,6 @@ from lzwcai_mcp_agile_db.server import main import os if __name__ == "__main__": - os.environ["AGILE_DB_API_KEY"] = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjJiMDY0YzMzLTBiZWYtNDU0NC04NWY1LTRmNTFiOGMxMmI5NSJ9.Uv599TvlQvlTlwrnZGo3Tl2eLAvM0ldE9vpMI5jHxbTf4_tVSRA60rUNIV4LBiw6pt1r_xIi7aFcTRE2PeN5sg" - os.environ["AGILE_DB_BASE_URL"] = "https://dempdemo.lzwcai.com" + os.environ["API_KEY"] = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjJiMDY0YzMzLTBiZWYtNDU0NC04NWY1LTRmNTFiOGMxMmI5NSJ9.Uv599TvlQvlTlwrnZGo3Tl2eLAvM0ldE9vpMI5jHxbTf4_tVSRA60rUNIV4LBiw6pt1r_xIi7aFcTRE2PeN5sg" + os.environ["backendBaseUrl"] = "https://dempdemo.lzwcai.com" main() diff --git a/lzwcai_mcp_agile_db/pyproject.toml b/lzwcai_mcp_agile_db/pyproject.toml index 7262bad..a740844 100644 --- a/lzwcai_mcp_agile_db/pyproject.toml +++ b/lzwcai_mcp_agile_db/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "lzwcai-mcp-agile-db" -version = "0.1.0" +version = "0.1.3" description = "MCP server for database management platform with 33 tools for datasource, table, data, API key, and skill management" readme = "README.md" requires-python = ">=3.10"