feat(.kilo): 新增 AgileDB 数据库操作技能

新增 lzwcai-agile-db 技能文件,为 AI Agent 提供 AgileDB 数据库操作的场景化工作流指导。
该技能支持数据源浏览、表数据 CRUD、SQL 执行和 AI 生成表结构等功能,包含完整的工具列表和使用场景。
This commit is contained in:
2026-06-11 18:51:49 +08:00
parent 9c597c9b0d
commit 557361632c
21 changed files with 1807 additions and 97 deletions

View File

@@ -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` 只包含要更新的字段,不需要提供全部字段
- 插入数据时,自增主键不需要提供
- 如果操作涉及多行,使用批量操作或循环调用
---
## 场景 5AI 生成表结构
当用户需要创建新表但不知道如何设计表结构时,使用 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 自动识别和加载。

Binary file not shown.

View File

@@ -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 根据自然语言生成表结构 | 安全(仅生成,不创建) |
### 三、表数据 CRUD5 个工具)
| 工具 | 功能 | 危险等级 |
|------|------|----------|
| `query_table_data` | 查询表数据(分页) | 安全 |
| `insert_table_row` | 插入一行数据 | 中等 |
| `update_table_row` | 更新一行数据 | 中等 |
| `delete_table_rows` | 删除数据行(按主键) | **危险** |
| `export_table_excel` | 导出表数据为 Excelbase64 | 安全 |
### 四、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` 环境
---
## 场景 5AI 生成表结构
当用户需要创建新表但不知道如何设计表结构时,使用 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 编码的文件内容需要提供文件名
---
## 场景 7API 密钥管理
当用户需要管理 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`

Binary file not shown.

View File

@@ -0,0 +1,356 @@
---
name: mcp-tool-testing
description: MCP 工具通用测试技能。自动发现、执行和组合测试任何 MCP 服务器中的工具,支持场景自动创造和风险前置询问。
version: 0.1.0
---
# MCP Tool Testing Skill
这是一个通用的 MCPModel Context Protocol工具测试技能。适用于任何 MCP 环境,无论是自研的还是第三方的。
## 核心原则
1. **通用性**:不假设任何具体的服务器名称、工具名称或项目结构
2. **自动发现**:通过 MCP 协议本身获取当前环境中可用的所有工具
3. **智能执行**:能判断的入参自动执行,不能判断的询问用户
4. **场景自动创造**:根据工具自动匹配并创造测试场景,无需用户指定
5. **风险前置**:有风险的操作必须询问用户确认,绝不擅作主张
6. **结果汇总**:最终只返回执行工具的入参、出参和理解
## 执行流程
### 第一步:发现可用工具
通过 MCP 的 `tools/list` 方法获取当前环境中所有可用的工具。
**获取每个工具的信息:**
- 工具名称name
- 工具描述description
- 入参 schemainputSchema
- 参数名称
- 参数类型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. **五类风险**:删除、泄密、政治、似黄、生产环境影响——必须询问用户

View File

@@ -6,8 +6,8 @@
| 变量名 | 必填 | 说明 | | 变量名 | 必填 | 说明 |
|--------|------|------| |--------|------|------|
| `AGILE_DB_API_KEY` | 是 | 数据库管理平台的 API 密钥 | | `API_KEY` | 是 | 数据库管理平台的 API 密钥(格式: `Bearer <token>` |
| `AGILE_DB_BASE_URL` | 否 | 数据库管理平台后端地址(默认 `http://localhost:8080` | | `backendBaseUrl` | 否 | 数据库管理平台后端地址(默认 `http://lzwcai-demp-corp-manager:8086` |
## 安装 ## 安装
@@ -19,8 +19,8 @@ pip install -e .
```bash ```bash
# 设置环境变量 # 设置环境变量
export AGILE_DB_API_KEY="your-api-key" export API_KEY="Bearer your-token"
export AGILE_DB_BASE_URL="http://localhost:8080" # 可选 export backendBaseUrl="https://dempdemo.lzwcai.com" # 可选
# 运行 MCP Server # 运行 MCP Server
lzwcai-mcp-agile-db lzwcai-mcp-agile-db

View File

@@ -1,6 +0,0 @@
def main():
print("Hello from lzwcai-mcp-agile-db!")
if __name__ == "__main__":
main()

View File

@@ -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 = []

View File

@@ -26,7 +26,7 @@ def register_tool(name: str):
input_schema = {...} input_schema = {...}
async def execute(self, args): async def execute(self, args):
return await self.client.get("/api/...", params=args) return await self.client.get("/datasource/...", params=args)
Args: Args:
name: 工具名称(唯一标识) name: 工具名称(唯一标识)

View File

@@ -22,7 +22,7 @@ class ListApiKeysTool(ToolDef):
async def execute(self, args: dict) -> dict: async def execute(self, args: dict) -> dict:
args = dict(args) args = dict(args)
params = {k: v for k, v in args.items() if v is not None} 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") @register_tool("create_api_key")
@@ -38,7 +38,7 @@ class CreateApiKeyTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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") @register_tool("toggle_api_key_status")
@@ -55,7 +55,7 @@ class ToggleApiKeyStatusTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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") @register_tool("delete_api_key")
@@ -71,7 +71,7 @@ class DeleteApiKeyTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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") @register_tool("get_api_key_permissions")
@@ -87,7 +87,7 @@ class GetApiKeyPermissionsTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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") @register_tool("grant_api_key_permissions")
@@ -118,4 +118,4 @@ class GrantApiKeyPermissionsTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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)

View File

@@ -38,8 +38,8 @@ class PreviewImportDataTool(ToolDef):
"file": (file_name, io.BytesIO(file_content), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), "file": (file_name, io.BytesIO(file_content), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
} }
return self.client.upload( return await self.client.upload(
f"/api/datasource/connection/{connection_id}/import_document/preview", f"/datasource/connection/{connection_id}/import_document/preview",
files=files, files=files,
params={"target": target}, params={"target": target},
) )
@@ -65,8 +65,8 @@ class ConfirmImportDataTool(ToolDef):
target = args.pop("target", "test") target = args.pop("target", "test")
data = args.pop("data") data = args.pop("data")
return self.client.post( return await self.client.post(
f"/api/datasource/connection/{connection_id}/import_document/confirm", f"/datasource/connection/{connection_id}/import_document/confirm",
json_data=data, json_data=data,
params={"target": target}, params={"target": target},
) )

View File

@@ -22,7 +22,7 @@ class ListDatabasesTool(ToolDef):
async def execute(self, args: dict) -> dict: async def execute(self, args: dict) -> dict:
params = {k: v for k, v in args.items() if v is not None} 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") @register_tool("list_tables")
@@ -42,7 +42,7 @@ class ListTablesTool(ToolDef):
async def execute(self, args: dict) -> dict: async def execute(self, args: dict) -> dict:
params = {k: v for k, v in args.items() if v is not None} 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") @register_tool("get_table_detail")
@@ -59,7 +59,7 @@ class GetTableDetailTool(ToolDef):
async def execute(self, args: dict) -> dict: async def execute(self, args: dict) -> dict:
table_id = args["tableId"] 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") @register_tool("create_table")
@@ -98,7 +98,7 @@ class CreateTableTool(ToolDef):
async def execute(self, args: dict) -> dict: async def execute(self, args: dict) -> dict:
args = dict(args) args = dict(args)
connection_id = args.pop("connectionId") 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") @register_tool("alter_table")
@@ -131,7 +131,7 @@ class AlterTableTool(ToolDef):
async def execute(self, args: dict) -> dict: async def execute(self, args: dict) -> dict:
args = dict(args) args = dict(args)
connection_id = args.pop("connectionId") 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") @register_tool("generate_table_by_description")
@@ -148,4 +148,4 @@ class GenerateTableByDescriptionTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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)

View File

@@ -27,7 +27,7 @@ class ListDatasourcesTool(ToolDef):
async def execute(self, args: dict) -> dict: async def execute(self, args: dict) -> dict:
params = {k: v for k, v in args.items() if v is not None} 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") @register_tool("get_datasource_detail")
@@ -47,17 +47,17 @@ class GetDatasourceDetailTool(ToolDef):
result = {} result = {}
# 获取基本信息 # 获取基本信息
try: 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: except Exception as e:
result["detail"] = {"error": str(e)} result["detail"] = {"error": str(e)}
# 获取配置 # 获取配置
try: 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: except Exception as e:
result["config"] = {"error": str(e)} result["config"] = {"error": str(e)}
# 获取实时结构 # 获取实时结构
try: 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: except Exception as e:
result["structure"] = {"error": str(e)} result["structure"] = {"error": str(e)}
return result return result
@@ -100,12 +100,12 @@ class CreateDatasourceTool(ToolDef):
"password": args.get("password"), "password": args.get("password"),
"connectionType": args.get("connectionType", "user_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: if test_result.get("code") != 200:
return {"success": False, "error": f"连接测试失败: {test_result.get('msg', '未知错误')}"} 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") @register_tool("update_datasource")
@@ -128,7 +128,7 @@ class UpdateDatasourceTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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") @register_tool("toggle_datasource_status")
@@ -145,7 +145,7 @@ class ToggleDatasourceStatusTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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") @register_tool("delete_datasource")
@@ -164,10 +164,10 @@ class DeleteDatasourceTool(ToolDef):
ds_id = args["id"] ds_id = args["id"]
# 先尝试停用(仅忽略已停用等预期错误) # 先尝试停用(仅忽略已停用等预期错误)
try: 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: except Exception as e:
# 记录日志但继续删除 # 记录日志但继续删除
logger.debug(f"停用数据源失败(可能已停用): {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}")

View File

@@ -20,7 +20,7 @@ class GetSkillByDatasourceTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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") @register_tool("get_skill_tools")
@@ -36,7 +36,7 @@ class GetSkillToolsTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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") @register_tool("create_skill")
@@ -54,7 +54,7 @@ class CreateSkillTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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") @register_tool("create_sql_tool")
@@ -97,7 +97,7 @@ class CreateSqlToolTool(ToolDef):
for suggestion in args["suggestions"]: for suggestion in args["suggestions"]:
if "sqlParams" in suggestion and isinstance(suggestion["sqlParams"], dict): if "sqlParams" in suggestion and isinstance(suggestion["sqlParams"], dict):
suggestion["sqlParams"] = json.dumps(suggestion["sqlParams"]) 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") @register_tool("delete_skill_tool")
@@ -113,7 +113,7 @@ class DeleteSkillToolTool(ToolDef):
} }
async def execute(self, args: dict) -> dict: 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") @register_tool("update_skill_config")
@@ -134,4 +134,4 @@ class UpdateSkillConfigTool(ToolDef):
# 如果 configTemplate 是 dict转为 JSON 字符串 # 如果 configTemplate 是 dict转为 JSON 字符串
if "configTemplate" in args and isinstance(args["configTemplate"], dict): if "configTemplate" in args and isinstance(args["configTemplate"], dict):
args["configTemplate"] = json.dumps(args["configTemplate"]) 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)

View File

@@ -13,13 +13,26 @@ class ExecuteSqlTool(ToolDef):
"type": "object", "type": "object",
"properties": { "properties": {
"datasourceId": {"type": "string", "description": "数据源 ID"}, "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 模板(可选)"}, "sqlTemplate": {"type": "string", "description": "SQL 模板(可选)"},
"businessName": {"type": "string", "description": "业务名称(可选)"}, "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: 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)

View File

@@ -12,12 +12,18 @@ class ToggleTableSubscriptionTool(ToolDef):
input_schema = { input_schema = {
"type": "object", "type": "object",
"properties": { "properties": {
"configId": {"type": "string", "description": "数据库配置 ID"}, "tableId": {"type": "string", "description": " ID"},
"tableName": {"type": "string", "description": "表名"}, "datasourceId": {"type": "string", "description": "数据源 ID"},
"isSubscribe": {"type": "boolean", "description": "true=订阅, false=取消订阅"}, "subscribe": {"type": "boolean", "description": "true=订阅, false=取消订阅"},
}, },
"required": ["configId", "tableName", "isSubscribe"], "required": ["tableId", "datasourceId", "subscribe"],
} }
async def execute(self, args: dict) -> dict: 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)

View File

@@ -26,7 +26,7 @@ class QueryTableDataTool(ToolDef):
args = dict(args) args = dict(args)
table_id = args.pop("tableId") table_id = args.pop("tableId")
params = {k: v for k, v in args.items() if v is not None} 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") @register_tool("insert_table_row")
@@ -49,7 +49,7 @@ class InsertTableRowTool(ToolDef):
target = args.pop("target", "prod") target = args.pop("target", "prod")
data = args.pop("data", {}) data = args.pop("data", {})
params = {"target": target} if target else {} 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") @register_tool("update_table_row")
@@ -75,7 +75,7 @@ class UpdateTableRowTool(ToolDef):
data = args.pop("data", {}) data = args.pop("data", {})
params = {"target": target} if target else {} params = {"target": target} if target else {}
body = {"primaryKey": primary_key, "data": data} 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") @register_tool("delete_table_rows")
@@ -103,7 +103,7 @@ class DeleteTableRowsTool(ToolDef):
primary_keys = args.pop("primaryKeys") primary_keys = args.pop("primaryKeys")
params = {"target": target} if target else {} params = {"target": target} if target else {}
body = {"primaryKeys": primary_keys} 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") @register_tool("export_table_excel")
@@ -124,7 +124,7 @@ class ExportTableExcelTool(ToolDef):
table_id = args.pop("tableId") table_id = args.pop("tableId")
target = args.pop("target", "prod") target = args.pop("target", "prod")
params = {"target": target} if target else {} 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"): if result.get("raw"):

View File

@@ -30,8 +30,8 @@ class AgileDBAPIClient:
初始化 API 客户端 初始化 API 客户端
Args: Args:
base_url: API 基础 URL默认从环境变量 AGILE_DB_BASE_URL 读取) base_url: API 基础 URL默认从环境变量 backendBaseUrl 读取)
api_key: API 密钥(默认从环境变量 AGILE_DB_API_KEY 读取) api_key: API 密钥(默认从环境变量 API_KEY 读取)
default_timeout: 请求超时时间(秒),默认 30 秒 default_timeout: 请求超时时间(秒),默认 30 秒
""" """
if base_url is None: if base_url is None:
@@ -43,15 +43,15 @@ class AgileDBAPIClient:
self.base_url = base_url.rstrip('/') self.base_url = base_url.rstrip('/')
self.api_key = api_key self.api_key = api_key
self.default_timeout = default_timeout 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}") logger.info(f"[客户端初始化] base_url={self.base_url}")
@property @property
def client(self) -> httpx.Client: def client(self) -> httpx.AsyncClient:
"""懒加载 HTTP 客户端""" """懒加载 HTTP 客户端"""
if self._client is None: if self._client is None:
self._client = httpx.Client(timeout=self.default_timeout) self._client = httpx.AsyncClient(timeout=self.default_timeout)
return self._client return self._client
def _get_headers(self, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]: def _get_headers(self, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
@@ -64,10 +64,14 @@ class AgileDBAPIClient:
return headers return headers
def _build_url(self, path: str) -> str: def _build_url(self, path: str) -> str:
"""构建完整 URL""" """构建完整 URL,自动去掉路径中多余的 /api 前缀"""
if path.startswith('http://') or path.startswith('https://'): if path.startswith('http://') or path.startswith('https://'):
return path 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]: def _handle_response(self, response: httpx.Response, url: str) -> Dict[str, Any]:
"""统一处理 API 响应""" """统一处理 API 响应"""
@@ -93,12 +97,12 @@ class AgileDBAPIClient:
return data 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 请求""" """发送 GET 请求"""
url = self._build_url(path) url = self._build_url(path)
try: try:
logger.info(f"[API请求] GET {url}") 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) return self._handle_response(response, url)
except httpx.TimeoutException: except httpx.TimeoutException:
raise Exception(f"API 请求超时: {url}") raise Exception(f"API 请求超时: {url}")
@@ -107,13 +111,13 @@ class AgileDBAPIClient:
except httpx.RequestError as e: except httpx.RequestError as e:
raise Exception(f"API 请求异常: {url}, 错误: {str(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 请求""" """发送 POST 请求"""
url = self._build_url(path) url = self._build_url(path)
try: try:
logger.info(f"[API请求] POST {url}") logger.info(f"[API请求] POST {url}")
headers = self._get_headers({'Content-Type': 'application/json'}) 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) return self._handle_response(response, url)
except httpx.TimeoutException: except httpx.TimeoutException:
raise Exception(f"API 请求超时: {url}") raise Exception(f"API 请求超时: {url}")
@@ -122,13 +126,13 @@ class AgileDBAPIClient:
except httpx.RequestError as e: except httpx.RequestError as e:
raise Exception(f"API 请求异常: {url}, 错误: {str(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 请求""" """发送 PUT 请求"""
url = self._build_url(path) url = self._build_url(path)
try: try:
logger.info(f"[API请求] PUT {url}") logger.info(f"[API请求] PUT {url}")
headers = self._get_headers({'Content-Type': 'application/json'}) 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) return self._handle_response(response, url)
except httpx.TimeoutException: except httpx.TimeoutException:
raise Exception(f"API 请求超时: {url}") raise Exception(f"API 请求超时: {url}")
@@ -137,7 +141,7 @@ class AgileDBAPIClient:
except httpx.RequestError as e: except httpx.RequestError as e:
raise Exception(f"API 请求异常: {url}, 错误: {str(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 请求""" """发送 DELETE 请求"""
url = self._build_url(path) url = self._build_url(path)
try: try:
@@ -145,7 +149,7 @@ class AgileDBAPIClient:
headers = self._get_headers() headers = self._get_headers()
if json_data is not None: if json_data is not None:
headers['Content-Type'] = 'application/json' 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) return self._handle_response(response, url)
except httpx.TimeoutException: except httpx.TimeoutException:
raise Exception(f"API 请求超时: {url}") raise Exception(f"API 请求超时: {url}")
@@ -154,14 +158,14 @@ class AgileDBAPIClient:
except httpx.RequestError as e: except httpx.RequestError as e:
raise Exception(f"API 请求异常: {url}, 错误: {str(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""" """发送文件上传请求multipart/form-data"""
url = self._build_url(path) url = self._build_url(path)
try: try:
logger.info(f"[API请求] UPLOAD {url}") logger.info(f"[API请求] UPLOAD {url}")
# 文件上传不需要 Content-Typehttpx 会自动设置 multipart/form-data # 文件上传不需要 Content-Typehttpx 会自动设置 multipart/form-data
headers = self._get_headers() 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) return self._handle_response(response, url)
except httpx.TimeoutException: except httpx.TimeoutException:
raise Exception(f"API 请求超时: {url}") raise Exception(f"API 请求超时: {url}")
@@ -170,17 +174,17 @@ class AgileDBAPIClient:
except httpx.RequestError as e: except httpx.RequestError as e:
raise Exception(f"API 请求异常: {url}, 错误: {str(e)}") raise Exception(f"API 请求异常: {url}, 错误: {str(e)}")
def close(self): async def close(self):
"""关闭 HTTP 客户端""" """关闭 HTTP 客户端"""
if self._client is not None: if self._client is not None:
self._client.close() await self._client.aclose()
self._client = None self._client = None
def __enter__(self): async def __aenter__(self):
return self return self
def __exit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):
self.close() await self.close()
return False return False

View File

@@ -6,7 +6,7 @@ from typing import Optional
def get_api_key(default: Optional[str] = None) -> str: def get_api_key(default: Optional[str] = None) -> str:
""" """
获取数据库管理平台 API 密钥 获取 API 密钥
Args: Args:
default: 默认值(可选) default: 默认值(可选)
@@ -15,25 +15,25 @@ def get_api_key(default: Optional[str] = None) -> str:
str: API 密钥 str: API 密钥
Raises: 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: if not value:
raise ValueError("环境变量 AGILE_DB_API_KEY 未设置") raise ValueError("环境变量 API_KEY 未设置")
return value 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: Args:
default: 默认值(默认 http://localhost:8080 default: 默认值(默认 http://lzwcai-demp-corp-manager:8086
Returns: Returns:
str: 后端 API 基础 URL str: 后端 API 基础 URL
""" """
return os.environ.get("AGILE_DB_BASE_URL", default) return os.environ.get("backendBaseUrl", default)
def get_env_config() -> dict: def get_env_config() -> dict:

View File

@@ -7,6 +7,6 @@ from lzwcai_mcp_agile_db.server import main
import os import os
if __name__ == "__main__": if __name__ == "__main__":
os.environ["AGILE_DB_API_KEY"] = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjJiMDY0YzMzLTBiZWYtNDU0NC04NWY1LTRmNTFiOGMxMmI5NSJ9.Uv599TvlQvlTlwrnZGo3Tl2eLAvM0ldE9vpMI5jHxbTf4_tVSRA60rUNIV4LBiw6pt1r_xIi7aFcTRE2PeN5sg" os.environ["API_KEY"] = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjJiMDY0YzMzLTBiZWYtNDU0NC04NWY1LTRmNTFiOGMxMmI5NSJ9.Uv599TvlQvlTlwrnZGo3Tl2eLAvM0ldE9vpMI5jHxbTf4_tVSRA60rUNIV4LBiw6pt1r_xIi7aFcTRE2PeN5sg"
os.environ["AGILE_DB_BASE_URL"] = "https://dempdemo.lzwcai.com" os.environ["backendBaseUrl"] = "https://dempdemo.lzwcai.com"
main() main()

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "lzwcai-mcp-agile-db" 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" description = "MCP server for database management platform with 33 tools for datasource, table, data, API key, and skill management"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"