Compare commits

15 Commits

Author SHA1 Message Date
635313a7ab ```
feat(lzwcai-agile-db): 更新版本至0.4.4并优化数据库管理技能文档

- 更新版本号从0.4.2到0.4.4
- 优化API密钥权限管理说明,明确grant_api_key_permissions仅支持追加不支持撤销
- 新增add_sql_tool_to_datasource工具,提供一键创建SQL工具功能
- 调整create_sql_tool说明,强调需技能已存在
- 强化数据写操作安全机制,插入/更新/删除前必须预览并等待用户确认
- 完善导入数据功能说明,详细解释confirm_import_data参数传递方式
- 补充技能与工具管理流程,提供更清晰的操作指引
- 新增数字员工平台数据库技能配置指南文档
```
2026-06-26 16:21:41 +08:00
ba5cd4bbe1 feat(lzwcai-agile-db): 更新AgileDB技能至v0.4.2版本并扩展工具集
- 将技能版本从0.2.0升级至0.4.2
- 工具数量从33个扩展至57个,新增数据源管理、AI训练、库表关联配置等功能
- 新增MQTT字段关联同步模块(8个工具)和库表关联配置(3个工具)
- 添加重要的契约提示和安全确认原则,包括target默认值、alter_table操作限制等
- 修正工具参数说明,如execute_sql的executableSql改为sql,参数结构优化
- 增强安全机制,明确危险操作的用户确认流程和目标资源选择规则
- 更新README.md中的工具数量统计和功能描述
2026-06-17 14:40:43 +08:00
557361632c feat(.kilo): 新增 AgileDB 数据库操作技能
新增 lzwcai-agile-db 技能文件,为 AI Agent 提供 AgileDB 数据库操作的场景化工作流指导。
该技能支持数据源浏览、表数据 CRUD、SQL 执行和 AI 生成表结构等功能,包含完整的工具列表和使用场景。
2026-06-11 18:51:49 +08:00
9c597c9b0d feat: 添加数据库管理平台MCP Server
新增lzwcai-mcp-agile-db项目,提供数据库管理、表操作、数据CRUD、
API密钥管理、技能与工具管理等功能。

包含33个工具:
- 数据源管理:创建、更新、删除数据源
- 数据库与表管理:表结构操作、数据查询等
- API密钥管理:密钥创建、权限管理等
- 技能与工具管理:SQL工具创建、配置更新等
- 数据导入和SQL执行功能

添加了完整的README文档说明安装使用方法,
以及Python 3.12版本支持和基本项目结构。
2026-06-11 09:53:40 +08:00
a1012e61bf feat: 新增userId追踪支持并更新版本至0.1.12
- 自动为生成的输入JSON Schema添加userId字段
- 从工具调用参数中提取userId并传递至API请求
- 新增userId日志打印与调试信息
- 修复API客户端无用的f-string日志调用
- 清理无用的类型导入项
2026-05-25 14:42:46 +08:00
fb61ae27cf feat: 新增HTML转URL工具服务,优化API转换器配置
- 新增lzwcai_mcpskills_visual2url项目,提供本地HTML文件、HTML代码转可访问URL的MCP工具
- 重构lzwcai_mcp_api_converter配置加载逻辑,取消内存缓存复用,强制拉取最新配置并新增缓存降级容错机制
- 升级lzwcai_mcp_api_converter至0.2.5、项目模板至0.1.3
- 更新各示例项目的环境配置参数与模板工具配置
2026-05-25 11:48:12 +08:00
992d97c0a4 feat(core): 改进API响应处理支持纯文本JSON解析
当API返回content-type为text/plain但实际内容是JSON格式时,
现在会尝试解析为JSON对象而不是直接返回文本,
提高API响应的兼容性

fix(create_mcp): 移除硬编码的输出schema定义

移除了工具注册中硬编码的outputSchema定义,
让系统使用更灵活的配置方式

chore(release): 更新版本号至0.2.4
2026-03-06 18:02:41 +08:00
8703a61198 feat(api-converter): 更新API配置并调整持久化令牌策略
- 替换api_config_9p04kww1pu.json为新的api_config_w8kgb73ib3.json配置文件,
  包含登录和单据查询两个API接口

- 修改AuthService类中的persist_token默认值为False,调整令牌持久化策略

- 移除旧的包信息文件、依赖文件和日志文件

- 更新API配置以支持金蝶K3Cloud系统的登录和单据查询功能
2026-03-06 16:19:07 +08:00
3c9fba36e9 feat(main): 修改 workflow extraContext 参数名称
将 workflow[extraContext] 参数名更改为 workflow_extraContext,
以避免方括号在某些系统中可能引起的解析问题。

同时更新了版本号从 0.1.7 到 0.1.8
2026-02-09 20:10:21 +08:00
32bc05376f feat(main): 修改工具输入schema以支持workflow[extraContext]字段
- 将params字段替换为workflow[extraContext]字段用于接收工作流额外上下文参数
- 更新描述信息,明确字段用途为接收工作流额外上下文参数(如环境变量等)
- 修改处理逻辑,提取workflow[extraContext]字段并合并到inputs中
- 当workflow[extraContext]为字典类型时合并到inputs,否则保留为独立字段
- 更新日志信息中的字段名称引用

chore(pyproject): 更新版本号至0.1.7
2026-02-09 19:44:51 +08:00
1b850913e7 feat(workflow): 添加 params 参数支持并优化工具调用处理
- 在输入 schema 中添加 params 字段,允许接收任何类型的额外参数
- 修改 handle_call_tool 函数,提取并合并 params 到 inputs
- 支持字典类型 params 的自动合并和其他类型 params 的保留
- 更新版本号从 0.1.5 到 0.1.6
2026-02-09 18:51:26 +08:00
a50aa307ab feat(schema-converter): 新增文件上传类型支持并优化schema转换逻辑
- 新增 file 和 fileList 类型用于单文件和多文件上传功能
- 添加文件配置信息处理,包括 accept、typeCategories、uploadMode 等参数
- 为文件类型添加 x-file-type、x-accept、x-type-categories 等扩展属性
- 优化默认值处理逻辑,排除文件类型设置默认值的情况

refactor(main): 重构工具配置处理优先级

- 将 schema 获取优先级调整为先从 sqlParams 转换,再使用 inputJsonSchema
- 增强错误处理机制,当 sqlParams 转换失败时自动回退到 inputJsonSchema
- 当两种方式都失败时提供空 schema 作为兜底方案
- 改进日志记录,增加调试信息和警告提示

chore: 更新项目版本号和测试配置

- 更新 pyproject.toml 中的版本号从 0.1.3 到 0.1.5
- 修改测试用的 workflowId 和 workflowExecuteKey 环境变量
2026-02-07 20:17:10 +08:00
e18c661368 feat(api-converter): 添加进销存采购订单API配置并实现本地缓存机制
新增api_config_9p04kww1pu.json配置文件,包含进销存采购订单相关的四个核心
API接口(查询列表、新建、详情、编辑),完善了load_api_configs函数,
增加本地文件缓存机制,支持从本地文件加载配置并在配置变更时同步保存,
优化refresh_api_configs函数以同步清理本地文件缓存。

BREAKING CHANGE: API配置方式调整,引入本地缓存机制可能影响原有部署流程
2026-02-07 15:48:01 +08:00
41c3d7a8fd fix(pyproject): 降低Python版本要求从3.13到3.10
修改了pyproject.toml文件中的Python版本要求,将最低支持版本从3.13降级到3.10,
同时更新了分类器中对应的Python版本声明,以提高项目的兼容性。
2026-01-28 09:36:17 +08:00
5107fdb74c chore(general): 更新项目配置文件
- 添加必要的配置项
- 优化现有设置
- 确保环境兼容性
2026-01-28 09:35:48 +08:00
191 changed files with 19404 additions and 1385 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.

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,343 @@
# 数字员工平台数据库技能配置指南
## 概述
本文档介绍两种使用数字员工平台数据库技能的方法:
- **方法一**:通过数字员工对话直接使用
- **方法二**:通过 AI 编辑器的 Skills + MCP 搭配使用
---
## 方法一:通过数字员工对话使用
### 配置说明
此方法通过配置 MCP Server 来连接数字员工平台。
### MCP 配置示例
```json
{
"mcpServers": {
"lzwcai-mcp-agile-db": {
"command": "uvx",
"type": "stdio",
"args": [
"lzwcai-mcp-agile-db"
],
"timeout": 600,
"env": {
"API_KEY": "eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjljMDllMjZhLWFkNzgtNGNmMi05YzQ5LWQzY2Y5NjI5MGRjYyJ9.v5ZffkvM2CnMkoWqc-Xy-0gN2oLBjyfJlm_YnCE6TWBwfKVmpuakoRXFMTiHQG8LOphv7JlEmZHcYpzwM52D0Q"
}
}
}
}
```
### 注意事项
- 配置中的 `API_KEY` 是数字员工平台的密钥
- 存在单点登录问题,请使用不会过期的账号密钥
- Skills 配置参考:`lzwcai-agile-db`
---
## 方法二:通过 AI 编辑器使用
### 配置说明
此方法通过下载 Skills 和 MCP 配置,在 AI 编辑器中搭配使用,支持直接对话操作。
### MCP 配置示例
```json
{
"mcpServers": {
"lzwcai_mcp_agile_db": {
"command": "uvx",
"args": [
"lzwcai-mcp-agile-db"
],
"env": {
"UV_INDEX_URL": "http://192.168.2.236:3141/lzwc/dev/+simple/",
"API_KEY": "eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjljMDllMjZhLWFkNzgtNGNmMi05YzQ5LWQzY2Y5NjI5MGRjYyJ9.v5ZffkvM2CnMkoWqc-Xy-0gN2oLBjyfJlm_YnCE6TWBwfKVmpuakoRXFMTiHQG8LOphv7JlEmZHcYpzwM52D0Q",
"backendBaseUrl": "http://192.168.2.236:8088"
}
}
}
}
```
### 环境变量说明
| 变量名 | 说明 | 示例值 |
|--------|------|--------|
| `UV_INDEX_URL` | Python 包索引地址 | `http://192.168.2.236:3141/lzwc/dev/+simple/` |
| `API_KEY` | 数字员工平台 API 密钥 | (见配置示例) |
| `backendBaseUrl` | 后端服务基础地址 | `http://192.168.2.236:8088` |
---
## 使用建议
1. **选择方法**:根据实际使用场景选择合适的方法
- 数字员工对话:适合快速使用
- AI 编辑器:适合更灵活的配置和使用
2. **密钥管理**:妥善保管 `API_KEY`,避免泄露
3. **网络配置**:确保网络可以访问配置的后端地址
---
## 相关资源
- Skills 目录:`lzwcai-agile-db/`
- 详细技能文档:`lzwcai-agile-db/SKILL.md`
---
## 对话使用场景示例
以下是配置好技能后,您可以直接使用的对话示例:
### 📊 场景 1浏览数据源和表
**用户:** "帮我看看有哪些数据源"
**AI 回复:**
```
共找到 3 个数据源:
1. HMD产品PostgreSQL, 运行中)
2. 测试数据库MySQL, 运行中)
3. 历史数据Oracle, 已停止)
请告诉我您想看哪个数据源?
```
---
**用户:** "看看HMD产品有哪些表"
**AI 回复:**
```
HMD产品 数据源包含以下数据库和表:
order_db:
- orders (订单表, 15 个字段, 2568 条记录)
- users (用户表, 8 个字段, 156 条记录)
- products (商品表, 12 个字段, 892 条记录)
analytics_db:
- daily_stats (日报表, 10 个字段, 365 条记录)
```
---
### 🔍 场景 2查询表数据
**用户:** "查一下users表前10条数据"
**AI 回复:**
```
users 表共 156 条记录,当前显示第 1-10 条:
| id | username | email | status | created_at |
|----|----------|-----------------|--------|---------------------|
| 1 | admin | admin@test.com | active | 2024-01-01 00:00:00 |
| 2 | user1 | user1@test.com | active | 2024-01-02 10:30:00 |
| 3 | user2 | user2@test.com | inactive| 2024-01-03 15:45:00 |
...
注:默认查询测试环境,如需查询生产环境请说明。
```
---
### 📝 场景 3执行 SQL 查询
**用户:** "统计每个地区的订单数量"
**AI 回复:**
```
各地区订单统计:
| 地区 | 订单数 |
|------|--------|
| 华东 | 1,250 |
| 华南 | 980 |
| 华北 | 756 |
| 西南 | 432 |
| 东北 | 289 |
```
---
### 场景 4插入数据
**用户:** "新增一个用户用户名是test_user邮箱test@test.com"
**AI 回复:**
```
即将向 users 表插入以下数据:
username = test_user
email = test@test.com
id 自动生成)
确认插入吗?
```
**用户:** "确认"
**AI 回复:**
```
✅ 已成功插入用户 test_user (ID: 157)
```
---
### ✏️ 场景 5更新数据
**用户:** "把ID为5的用户邮箱改成new@test.com"
**AI 回复:**
```
即将更新 users 表 ID=5 的记录:
email: old@test.com → new@test.com
确认更新吗?
```
**用户:** "确认"
**AI 回复:**
```
✅ 已更新用户 ID=5 的邮箱为 new@test.com
```
---
### 🗑️ 场景 6删除数据
**用户:** "删除ID为10的订单"
**AI 回复:**
```
⚠️ 安全提醒:此操作存在删除风险。
具体说明:数据将永久删除,不可恢复。
确认要删除以下记录吗?
订单 ID=10, 订单号=ORD-2024-0010, 金额=¥1,250.00
请回复"确认删除"继续。
```
**用户:** "确认删除"
**AI 回复:**
```
✅ 已成功删除订单 ID=10
```
---
### 🤖 场景 7AI 生成表结构
**用户:** "帮我设计一个商品表,包含名称、价格、库存、分类"
**AI 回复:**
```
AI 已生成表结构设计:
表名: products (商品表)
字段:
- id (SERIAL, 主键) - 主键ID
- name (VARCHAR(200), 非空) - 商品名称
- price (DECIMAL(10,2)) - 价格
- stock (INTEGER) - 库存数量
- category (VARCHAR(50)) - 分类
- created_at (TIMESTAMP) - 创建时间
是否需要在此基础上修改或直接创建?
```
**用户:** "直接创建到order_db数据库"
**AI 回复:**
```
✅ 已成功创建表 products (商品表)
```
---
### 📥 场景 8导入 Excel 数据
**用户:** "帮我导入这个Excel文件https://example.com/data.xlsx"
**AI 回复:**
```
AI 已识别 Excel 文件内容:
表名: customers (客户表)
字段:
- id (SERIAL, 主键)
- name (VARCHAR(100)) - 客户名称
- phone (VARCHAR(20)) - 电话
- address (TEXT) - 地址
预览数据前3条
| id | name | phone | address |
|----|--------|------------|----------------|
| 1 | 张三 | 13800138000| 北京市朝阳区 |
| 2 | 李四 | 13900139000| 上海市浦东新区 |
| 3 | 王五 | 13700137000| 广州市天河区 |
共 50 条数据待导入。
⚠️ 安全提醒:此操作可能涉及数据安全风险。
说明:导入的数据将写入数据库,请确认数据来源合法合规。
请确认是否继续?
```
**用户:** "继续"
**AI 回复:**
```
✅ 成功导入 50 条数据到 customers 表
```
---
### 🔑 场景 9API 密钥管理
**用户:** "帮我创建一个新的API密钥"
**AI 回复:**
```
请输入密钥名称:
```
**用户:** "测试密钥"
**AI 回复:**
```
✅ 已创建 API 密钥:
名称: 测试密钥
密钥: lzwc_sk_xxxxxxxxxxxx
状态: 启用
过期时间: 2025-06-18
请妥善保管密钥,丢失后无法找回。
```
---
## ⚠️ 重要使用提示
1. **环境选择:** 默认使用测试环境(`test`),操作生产环境需要明确说明"查询生产环境"或"操作生产环境"
2. **安全确认:** 所有写操作(增删改)都会先预览并等待您的确认
3. **多选原则:** 有多个数据源/数据库/表可选时AI会列出让您选择不会擅自做主
4. **分步执行:** 复杂任务会分步完成,每一步都会确认后再继续

View File

@@ -0,0 +1,86 @@
# lzwcai-mcp-agile-db
数据库管理平台 MCP Server提供 34 个工具用于数据库管理、表操作、数据 CRUD、API 密钥管理、技能与工具管理等。
## 环境变量
| 变量名 | 必填 | 说明 |
|--------|------|------|
| `API_KEY` | 是 | 数据库管理平台的 API 密钥(格式: `Bearer <token>` |
| `backendBaseUrl` | 否 | 数据库管理平台后端地址(默认 `http://lzwcai-demp-corp-manager:8086` |
## 安装
```bash
pip install -e .
```
## 运行
```bash
# 设置环境变量
export API_KEY="Bearer your-token"
export backendBaseUrl="https://dempdemo.lzwcai.com" # 可选
# 运行 MCP Server
lzwcai-mcp-agile-db
```
## 工具列表
### 数据源管理
- `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` - 通过自然语言生成表结构
### 表数据 CRUD
- `query_table_data` - 查询表数据
- `insert_table_row` - 插入行数据
- `update_table_row` - 更新行数据
- `delete_table_rows` - 删除行数据
- `export_table_excel` - 导出 Excel
### API 密钥管理
- `list_api_keys` - 获取密钥列表
- `create_api_key` - 创建密钥
- `toggle_api_key_status` - 启用/禁用密钥
- `delete_api_key` - 删除密钥
- `get_api_key_permissions` - 查看密钥权限
- `grant_api_key_permissions` - 授予权限(仅追加,不可撤销)
### 技能与工具管理
- `add_sql_tool_to_datasource` - 把 SQL 沉淀为工具(一步到位,自动建技能+配模板+建工具,推荐入口)
- `get_skill_by_datasource` - 获取技能信息
- `get_skill_tools` - 获取技能工具列表
- `create_sql_tool` - 创建 SQL 工具(需技能已存在)
- `delete_skill_tool` - 删除技能工具
- `update_skill_config` - 更新技能配置
- `update_skill_tool` - 修改技能工具
### 数据导入
- `preview_import_data` - 预览导入数据
- `confirm_import_data` - 确认导入数据
### 表订阅与 SQL 执行
- `toggle_table_subscription` - 切换表订阅
- `execute_sql` - 执行 SQL 查询
## 架构
- `tools/_base.py` - 工具注册装饰器和基类
- `tools/*.py` - 工具实现文件
- `utils/api_client.py` - 统一 HTTP 客户端
- `utils/env_config.py` - 环境变量配置
- `utils/logger_config.py` - 日志配置
- `server.py` - MCP Server 注册和启动逻辑

View File

@@ -0,0 +1 @@
3.12

View File

@@ -0,0 +1,5 @@
"""lzwcai-mcp-agile-db MCP Server 包"""
from .server import main
__all__ = ["main"]

View File

@@ -0,0 +1,377 @@
2026-06-22 23:02:48 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-22 23:02:48 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-22 23:02:48 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:73] - [客户端初始化] base_url=https://dempdemo.lzwcai.com/api, 认证方式=account:demp04
2026-06-22 23:02:48 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:118] - [登录] POST https://dempdemo.lzwcai.com/api/login, username=demp04, loginType=user
2026-06-22 23:02:48 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x0000020F12F4F5C0>
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.started ssl_context=<ssl.SSLContext object at 0x0000020F12F165D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x0000020F12DFD970>
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Mon, 22 Jun 2026 15:02:58 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
2026-06-22 23:02:49 - httpx - INFO - [_client.py:1740] - HTTP Request: POST https://dempdemo.lzwcai.com/api/login "HTTP/1.1 200 "
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:161] - [API响应] HTTP 200
2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:137] - [登录] 成功获取 token
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
2026-06-23 09:47:20 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
2026-06-23 09:47:20 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001FB167DF860>
2026-06-23 09:47:21 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8088, 认证方式=account:yy8z9
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001FB156EB260>
2026-06-23 09:47:28 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8088/login, username=yy8z9, loginType=user
2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8088 local_address=None timeout=30.0 socket_options=None
2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000001FB16D14260>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')])
2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8088/login "HTTP/1.1 200 "
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8088/datasource/api_key/list
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')])
2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8088/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
"total": 21,
"rows": [
{
"createBy": "",
"createTime": "2026-06-18 11:29:46",
"updateBy": "",
"updateTime": "2026-06-18 11:59:25",
"remark": null,
"id": "37",
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
"apiKeyName": "盒马超市只读访问密钥",
"enterpriseId": "1932095424144715777",
"status": 0,
"expireTime": "2027-06-18T11:59:25.000+08:00"
},
{
"createBy": "",
"createTime": "2026-06-18 10:54:40",
...
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 09:47:33 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
2026-06-23 09:47:33 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
2026-06-23 09:48:33 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
2026-06-23 09:48:33 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001F89A81AAB0>
2026-06-23 09:48:34 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001F89A273B00>
2026-06-23 09:48:36 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000001F89C2B8F20>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
"total": 21,
"rows": [
{
"createBy": "",
"createTime": "2026-06-18 11:29:46",
"updateBy": "",
"updateTime": "2026-06-18 11:59:25",
"remark": null,
"id": "37",
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
"apiKeyName": "盒马超市只读访问密钥",
"enterpriseId": "1932095424144715777",
"status": 0,
"expireTime": "2027-06-18T11:59:25.000+08:00"
},
{
"createBy": "",
"createTime": "2026-06-18 10:54:40",
...
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
2026-06-23 11:11:10 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
2026-06-23 11:11:10 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCAFE606E0>
2026-06-23 11:11:11 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCB0FBFB60>
2026-06-23 11:11:14 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
2026-06-23 11:11:14 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
2026-06-23 11:11:14 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000002BCB15974D0>
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
"total": 21,
"rows": [
{
"createBy": "",
"createTime": "2026-06-18 11:29:46",
"updateBy": "",
"updateTime": "2026-06-18 11:59:25",
"remark": null,
"id": "37",
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
"apiKeyName": "盒马超市只读访问密钥",
"enterpriseId": "1932095424144715777",
"status": 0,
"expireTime": "2027-06-18T11:59:25.000+08:00"
},
{
"createBy": "",
"createTime": "2026-06-18 10:54:40",
...
2026-06-23 11:11:15 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCB0EA5D60>
2026-06-23 11:11:27 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000002BCB100F1D0>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json;charset=utf-8'), (b'Content-Length', b'51'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN')])
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:239] - [认证] 收到 401登录过期尝试重新登录后重试一次
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
"total": 21,
"rows": [
{
"createBy": "",
"createTime": "2026-06-18 11:29:46",
"updateBy": "",
"updateTime": "2026-06-18 11:59:25",
"remark": null,
"id": "37",
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
"apiKeyName": "盒马超市只读访问密钥",
"enterpriseId": "1932095424144715777",
"status": 0,
"expireTime": "2027-06-18T11:59:25.000+08:00"
},
{
"createBy": "",
"createTime": "2026-06-18 10:54:40",
...
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
2026-06-23 11:37:08 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-23 11:37:08 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401登录过期尝试重新登录后重试一次
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401登录过期尝试重新登录后重试一次
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401登录过期尝试重新登录后重试一次
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:314] - [认证] 重新登录后仍返回 401
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401登录过期尝试重新登录后重试一次
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200

View File

@@ -0,0 +1 @@
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:314] - [认证] 重新登录后仍返回 401

View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""
lzwcai-mcp-agile-db MCP Server
数据库管理平台 MCP 工具服务,提供数据库管理、表/数据操作、技能、API 密钥、MQTT 同步、AI 训练等工具
"""
import json
import logging
import anyio
import mcp.types as types
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from .utils.logger_config import setup_system_logging, get_logger
from .utils.api_client import AgileDBAPIClient
from .tools._base import get_registered_tools
# 初始化日志系统
setup_system_logging(app_name="lzwcai_mcp_agile_db", log_level=logging.DEBUG)
logger = get_logger(__name__)
# 初始化 MCP Server
server = Server("lzwcai_mcp_agile_db")
# 全局 API 客户端
_api_client: AgileDBAPIClient = None
def get_api_client() -> AgileDBAPIClient:
"""获取或创建 API 客户端"""
global _api_client
if _api_client is None:
_api_client = AgileDBAPIClient()
return _api_client
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""列出所有可用工具"""
logger.info("收到 ListTools 请求")
tools = []
for tool_cls in get_registered_tools():
instance = tool_cls(get_api_client())
tool_def = instance.to_tool_def()
tools.append(
types.Tool(
name=tool_def["name"],
description=tool_def["description"],
inputSchema=tool_def["inputSchema"],
)
)
logger.info(f"ListTools 响应: 返回 {len(tools)} 个工具")
return tools
@server.call_tool()
async def handle_call_tool(
name: str,
arguments: dict | None,
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""调用工具"""
logger.info(f"收到 CallTool 请求: name={name}")
# 查找对应的工具类
tool_cls = None
for cls in get_registered_tools():
if cls.name == name:
tool_cls = cls
break
if tool_cls is None:
logger.error(f"未找到工具: {name}")
raise ValueError(f"未知工具: {name}")
# 创建工具实例并执行
client = get_api_client()
tool_instance = tool_cls(client)
try:
result = await tool_instance.execute(arguments or {})
logger.info(f"工具执行成功: {name}")
logger.debug(f"工具返回结果: {json.dumps(result, ensure_ascii=False, indent=2)[:500]}...")
return [
types.TextContent(
type="text",
text=json.dumps(result, ensure_ascii=False, indent=2),
)
]
except Exception as e:
logger.error(f"工具执行失败: {name}, 错误: {e}", exc_info=True)
return [
types.TextContent(
type="text",
text=json.dumps({"error": str(e), "tool_name": name}, ensure_ascii=False, indent=2),
)
]
async def run_server():
"""运行 MCP Server (stdio 模式)"""
try:
async with stdio_server() as streams:
await server.run(
streams[0],
streams[1],
InitializationOptions(
server_name="lzwcai_mcp_agile_db",
server_version="0.1.8",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
finally:
# 释放全局 HTTP 客户端
if _api_client is not None:
await _api_client.close()
def main():
"""主入口"""
logger.info("=" * 50)
logger.info("lzwcai-mcp-agile-db MCP Server 启动")
# 导入所有工具模块(触发装饰器注册)
from . import tools # noqa: F401
logger.info(f"已注册工具数量: {len(get_registered_tools())}")
logger.info("=" * 50)
logger.info("开始运行 MCP Server (stdio 模式)")
anyio.run(run_server)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,18 @@
"""
工具自动发现模块
自动导入 tools/ 目录下所有工具模块,触发 @register_tool 装饰器注册
"""
import importlib
import pkgutil
from pathlib import Path
# 获取当前包路径
_package_path = Path(__file__).parent
# 遍历所有 Python 文件(排除 __init__.py 和 _base.py
for _, module_name, _ in pkgutil.iter_modules([str(_package_path)]):
if module_name.startswith("_"):
continue
# 动态导入模块,触发装饰器
importlib.import_module(f".{module_name}", __package__)

View File

@@ -0,0 +1,116 @@
"""
工具注册框架
提供 ToolDef 基类和 @register_tool 装饰器,用于声明式定义 MCP 工具
"""
from abc import ABC, abstractmethod
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from ..utils.api_client import AgileDBAPIClient
# 全局工具注册表
_registered_tools = []
def register_tool(name: str):
"""
工具注册装饰器
使用方式:
@register_tool("list_datasources")
class ListDatasourcesTool(ToolDef):
name = "list_datasources"
description = "获取数据源列表"
input_schema = {...}
async def execute(self, args):
return await self.client.get("/datasource/...", params=args)
Args:
name: 工具名称(唯一标识)
"""
def decorator(cls):
# 确保类有正确的 name 属性
if not hasattr(cls, 'name') or cls.name != name:
cls.name = name
# 注册到全局列表,避免重复注册
if cls not in _registered_tools:
_registered_tools.append(cls)
return cls
return decorator
class ToolDef(ABC):
"""
工具定义基类
所有工具都应继承此类并使用 @register_tool 装饰器注册
类属性:
name: 工具名称(唯一标识)
description: 工具描述
input_schema: JSON Schema 格式的工具输入参数定义
实例属性:
client: AgileDBAPIClient 实例(由 server 注入)
"""
name: str = ""
description: str = ""
input_schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
def __init__(self, client: "AgileDBAPIClient"):
"""
初始化工具实例
Args:
client: API 客户端实例(由 server 注入)
"""
self.client = client
@abstractmethod
async def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""
执行工具逻辑
Args:
args: 工具输入参数(已校验)
Returns:
Dict[str, Any]: 执行结果,将作为 MCP 工具返回值
"""
pass
def to_tool_def(self) -> Dict[str, Any]:
"""
转换为 MCP 工具定义格式
Returns:
dict: MCP types.Tool 所需的参数
"""
return {
"name": self.name,
"description": self.description,
"inputSchema": self.input_schema,
}
def get_registered_tools() -> list:
"""
获取所有已注册的工具类列表
Returns:
list: 所有被 @register_tool 装饰的类
"""
return list(_registered_tools)
def clear_registered_tools():
"""
清空所有已注册的工具(主要用于测试)
"""
_registered_tools.clear()

View File

@@ -0,0 +1,68 @@
"""
AI 补全训练工具
含 generate_table_by_description 的轮询配套 get_ai_training_detail。
训练状态 trainingStatus: 0 待训练 / 1 训练中 / 2 已完成 / 3 失败
"""
from ._base import register_tool, ToolDef
@register_tool("list_ai_trainings")
class ListAiTrainingsTool(ToolDef):
name = "list_ai_trainings"
description = "获取 AI 补全训练任务列表"
input_schema = {
"type": "object",
"properties": {
"datasourceId": {"type": "string", "description": "数据源 ID可选过滤"},
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
},
"required": [],
}
async def execute(self, args: dict) -> dict:
params = {k: v for k, v in args.items() if v is not None}
return await self.client.get("/ai/training/list", params=params)
@register_tool("create_ai_training_by_selected")
class CreateAiTrainingBySelectedTool(ToolDef):
name = "create_ai_training_by_selected"
description = "按选中的表创建 AI 补全训练任务"
input_schema = {
"type": "object",
"properties": {
"datasourceId": {"type": "string", "description": "数据源 ID"},
"tableIds": {
"type": "array",
"description": "要训练的表 ID 数组",
"items": {"type": "string"},
},
},
"required": ["tableIds"],
}
async def execute(self, args: dict) -> dict:
return await self.client.post("/ai/training/createBySelected", json_data=args)
@register_tool("get_ai_training_detail")
class GetAiTrainingDetailTool(ToolDef):
name = "get_ai_training_detail"
description = (
"查询 AI 训练任务详情(轮询用)。"
"trainingStatus: 0 待训练 / 1 训练中 / 2 已完成 / 3 失败。"
"generate_table_by_description 返回 taskId 后,用本工具轮询直到 status=2 取结果"
)
input_schema = {
"type": "object",
"properties": {
"taskId": {"type": "string", "description": "训练任务 IDgenerate_table 返回的 taskId"},
},
"required": ["taskId"],
}
async def execute(self, args: dict) -> dict:
return await self.client.get(f"/ai/training/{args['taskId']}")

View File

@@ -0,0 +1,124 @@
"""
API 密钥管理工具(含创建、状态切换、删除、权限查询/授予)
注意权限模型为「仅追加」——grant_api_key_permissions 只能新增权限,后端不支持撤销/删除
已授予的权限(真机验证 permission 删除接口返回「不支持当前的调用方式」),故不提供 revoke 工具。
"""
from ._base import register_tool, ToolDef
@register_tool("list_api_keys")
class ListApiKeysTool(ToolDef):
name = "list_api_keys"
description = "获取 API 密钥列表"
input_schema = {
"type": "object",
"properties": {
"apiKeyName": {"type": "string", "description": "密钥名称模糊搜索"},
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
},
"required": [],
}
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 await self.client.get("/datasource/api_key/list", params=params)
@register_tool("create_api_key")
class CreateApiKeyTool(ToolDef):
name = "create_api_key"
description = "创建新的 API 密钥"
input_schema = {
"type": "object",
"properties": {
"apiKeyName": {"type": "string", "description": "密钥名称最多50字"},
},
"required": ["apiKeyName"],
}
async def execute(self, args: dict) -> dict:
return await self.client.post("/datasource/api_key", json_data=args)
@register_tool("toggle_api_key_status")
class ToggleApiKeyStatusTool(ToolDef):
name = "toggle_api_key_status"
description = "启用/禁用 API 密钥"
input_schema = {
"type": "object",
"properties": {
"id": {"type": "string", "description": "密钥 ID"},
"status": {"type": "integer", "enum": [0, 1], "description": "0=启用, 1=禁用"},
},
"required": ["id", "status"],
}
async def execute(self, args: dict) -> dict:
return await self.client.put("/datasource/api_key", json_data=args)
@register_tool("delete_api_key")
class DeleteApiKeyTool(ToolDef):
name = "delete_api_key"
description = "删除 API 密钥"
input_schema = {
"type": "object",
"properties": {
"id": {"type": "string", "description": "密钥 ID"},
},
"required": ["id"],
}
async def execute(self, args: dict) -> dict:
return await self.client.delete(f"/datasource/api_key/{args['id']}")
@register_tool("get_api_key_permissions")
class GetApiKeyPermissionsTool(ToolDef):
name = "get_api_key_permissions"
description = "查看指定密钥的权限配置"
input_schema = {
"type": "object",
"properties": {
"apiKeyId": {"type": "string", "description": "密钥 ID"},
},
"required": ["apiKeyId"],
}
async def execute(self, args: dict) -> dict:
return await self.client.get(f"/datasource/api_key/permission/{args['apiKeyId']}")
@register_tool("grant_api_key_permissions")
class GrantApiKeyPermissionsTool(ToolDef):
name = "grant_api_key_permissions"
description = "批量为 API 密钥授予权限(仅追加,不会覆盖或删除已有权限;后端不支持撤销已授予的权限)"
input_schema = {
"type": "object",
"properties": {
"apiKeyId": {"type": "string", "description": "密钥 ID"},
"batchDatas": {
"type": "array",
"description": "权限批量数据数组",
"items": {
"type": "object",
"properties": {
"connectionId": {"type": "string", "description": "数据源 ID"},
"permissionLevel": {"type": "string", "enum": ["connection", "database", "table"], "description": "权限级别"},
"permissionType": {"type": "string", "description": "权限类型(逗号分隔)"},
"databaseName": {"type": "string", "description": "数据库名level=database/table 时)"},
"tableName": {"type": "string", "description": "表名level=table 时)"},
},
"required": ["connectionId", "permissionLevel", "permissionType"],
},
},
},
"required": ["apiKeyId", "batchDatas"],
}
async def execute(self, args: dict) -> dict:
return await self.client.post("/datasource/api_key/permission/grant_batch", json_data=args)

View File

@@ -0,0 +1,104 @@
"""
库表关联配置工具(外置/内置通用)
对应文档 §2.2 / §4.2
- postConnectionConfig 建库表关联配置(外置源建完连接后选库选表的最后一步)
- putConnectionConfig 改库表关联配置
- deleteConnectionConfig 删配置(同时也是「删除库」的实现)
"""
from ._base import register_tool, ToolDef
@register_tool("create_connection_config")
class CreateConnectionConfigTool(ToolDef):
name = "create_connection_config"
description = (
"创建「库表关联」配置:外置数据源建完连接后,选择要纳管的库与表的最后一步。"
"完成后数据源才能用于智能问数"
)
input_schema = {
"type": "object",
"properties": {
"connectionId": {"type": "string", "description": "数据源连接ID"},
"syncTables": {"type": "boolean", "default": True, "description": "是否同步表结构,默认 true"},
"databases": {
"type": "array",
"description": "要纳管的库及其表",
"items": {
"type": "object",
"properties": {
"databaseName": {"type": "string", "description": "库名"},
"tableNames": {
"type": "array",
"description": "选中的表名数组",
"items": {"type": "string"},
},
},
"required": ["databaseName", "tableNames"],
},
},
},
"required": ["connectionId", "databases"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
args.setdefault("syncTables", True)
return await self.client.post("/datasource/config", json_data=args)
@register_tool("update_connection_config")
class UpdateConnectionConfigTool(ToolDef):
name = "update_connection_config"
description = "更新「库表关联」配置(编辑已纳管的库表范围)"
input_schema = {
"type": "object",
"properties": {
"id": {"type": "string", "description": "配置 ID编辑时必传"},
"connectionId": {"type": "string", "description": "数据源连接ID"},
"syncTables": {"type": "boolean", "default": True, "description": "是否同步表结构,默认 true"},
"databases": {
"type": "array",
"description": "要纳管的库及其表",
"items": {
"type": "object",
"properties": {
"databaseName": {"type": "string", "description": "库名"},
"tableNames": {
"type": "array",
"description": "选中的表名数组",
"items": {"type": "string"},
},
},
"required": ["databaseName", "tableNames"],
},
},
},
"required": ["id", "connectionId", "databases"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
args.setdefault("syncTables", True)
return await self.client.put("/datasource/config", json_data=args)
@register_tool("delete_connection_config")
class DeleteConnectionConfigTool(ToolDef):
name = "delete_connection_config"
description = "删除库表关联配置(批量)。这也是「删除内置库」的实现:传入对应库的配置 ID"
input_schema = {
"type": "object",
"properties": {
"ids": {
"type": "array",
"description": "配置 ID 数组",
"items": {"type": "string"},
},
},
"required": ["ids"],
}
async def execute(self, args: dict) -> dict:
return await self.client.post("/datasource/config/deletes", json_data={"ids": args["ids"]})

View File

@@ -0,0 +1,324 @@
"""
数据导入工具 (工具 30-31)
"""
import io
import mimetypes
import os
from urllib.parse import urlparse, unquote
from ._base import register_tool, ToolDef
@register_tool("preview_import_data")
class PreviewImportDataTool(ToolDef):
name = "preview_import_data"
description = "从 URL 下载 Excel 文件AI 智能识别并预览表结构/数据"
input_schema = {
"type": "object",
"properties": {
"connectionId": {"type": "string", "description": "数据源 ID"},
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test"},
"file_url": {"type": "string", "description": "Excel 文件下载地址(.xlsx/.xls, <500KB"},
},
"required": ["connectionId", "file_url"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
connection_id = args.pop("connectionId")
target = args.pop("target", "test")
file_url = args.pop("file_url")
# 0. 基础 URL 校验
parsed_url = urlparse(file_url)
if parsed_url.scheme not in ("http", "https"):
raise ValueError("仅支持 http/https 协议的下载地址")
# 1. 下载文件
download_resp = await self.client.client.get(file_url, follow_redirects=True, timeout=30.0)
download_resp.raise_for_status()
# 1.1 预先根据 Content-Length 拦截超大文件,避免无意义下载
max_size = 500 * 1024
content_length = download_resp.headers.get("content-length")
if content_length:
try:
if int(content_length) > max_size:
raise ValueError(
f"文件 Content-Length {content_length} bytes 超过平台限制 {max_size} bytes (500KB)"
)
except ValueError as e:
if "超过平台限制" in str(e):
raise
# 非数字时忽略
file_content = download_resp.content
# 2. 文件大小校验(平台限制 500KB
if len(file_content) > max_size:
raise ValueError(
f"文件大小 {len(file_content)} bytes 超过平台限制 {max_size} bytes (500KB)"
"请先压缩或拆分后重试"
)
# 3. 解析文件名并校验扩展名
file_name = self._extract_file_name(download_resp, file_url)
allowed_exts = (".xlsx", ".xls")
if not file_name.lower().endswith(allowed_exts):
raise ValueError(
f"不支持的文件类型: {file_name},仅支持 {', '.join(allowed_exts)}"
)
# 4. 推断 MIME 类型
content_type, _ = mimetypes.guess_type(file_name)
if not content_type:
content_type = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
if file_name.lower().endswith(".xlsx")
else "application/vnd.ms-excel"
)
# 5. 构建文件上传
files = {
"file": (file_name, io.BytesIO(file_content), content_type),
}
return await self.client.upload(
f"/datasource/connection/{connection_id}/import_document/preview",
files=files,
params={"target": target},
)
@staticmethod
def _extract_file_name(response, file_url: str) -> str:
"""从响应头或 URL 中提取文件名。"""
# 优先从 Content-Disposition 解析
content_disposition = response.headers.get("content-disposition")
if content_disposition:
# 简单解析 filename="xxx" 或 filename*=UTF-8''xxx
# 注意filename* 也包含 filename= 前缀,必须先检查 filename*
for part in content_disposition.split(";"):
part = part.strip()
if part.lower().startswith("filename*="):
encoded = part.split("=", 1)[1]
# 形如 UTF-8''xxx
if "''" in encoded:
_, _, file_name = encoded.partition("''")
return os.path.basename(unquote(file_name, encoding="utf-8"))
return os.path.basename(unquote(encoded, encoding="utf-8"))
if part.lower().startswith("filename="):
file_name = part.split("=", 1)[1].strip('"')
return os.path.basename(unquote(file_name))
# fallback 从 URL 路径获取
parsed = urlparse(file_url)
path = unquote(parsed.path)
file_name = os.path.basename(path) if path else ""
if file_name and "." in file_name:
return file_name
return "import.xlsx"
@register_tool("confirm_import_data")
class ConfirmImportDataTool(ToolDef):
name = "confirm_import_data"
description = (
"确认导入 AI 识别后的数据(建表+插数据),第二步。第一步先调 preview_import_data。\n"
"【data 传什么】把 preview_import_data 返回的 data 原文整块传给 data 参数即可,"
"工具会自动解包并组装成后端要求的 {tableStructure(单表对象,含databaseName), allData} 结构。\n"
"data 的标准形态(= preview 的返回):\n"
" {\n"
" \"tableStructure\": { \"success\":true, \"message\":\"...\",\n"
" \"data\": { \"tables\": [ { \"tableName\":\"animals\", \"columns\":[...] } ] },\n"
" \"allData\": [ [列名表头行...], [行1各列值...], [行2各列值...] ] },\n"
" \"databaseName\": \"目标库名\", \"target\": \"prod|test\"\n"
" }\n"
"【allData 的结构关键】allData 是二维数组:\n"
" · 首行 allData[0] 是【表头行】= 各列的 columnName列名真实数据从 allData[1] 起;\n"
" · 每行(含表头行)都是「按 columns 顺序排列的位置数组」,行宽 = 列总数(含 SERIAL 主键等所有列,不裁剪);\n"
" · 若调用方传的 allData 没带表头行(首行就是数据),工具会据列名自动补一行表头——"
"否则后端会把首行数据当成字段名,报「查询字段不存在/字段名称不正确」。\n"
"【列名必须对应目标表真实字段】tableStructure.columns及 allData 首行表头)里的列名,"
"必须是目标表中确实存在的字段名(后端按列名拼 INSERT。导入到已有表时"
"不要直接用 Excel 识别出的列,应先调 get_table_detail 拿到目标表真实字段,"
"再把 columns、表头、各行取值对齐到这些真实字段否则报「查询字段不存在/字段名称不正确」。\n"
"databaseName/target 既可放顶层参数,也可放在 data 里,工具都能识别。"
)
input_schema = {
"type": "object",
"properties": {
"connectionId": {"type": "string", "description": "数据源连接 ID同 preview 用的那个)"},
"databaseName": {"type": "string", "description": "落库的数据库名(导入目标库)。必填——顶层不给会尝试从 data.databaseName 回捞"},
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test。可放顶层或 data 里"},
"data": {
"type": "object",
"description": (
"preview_import_data 返回的 data 原文整块(含 tableStructure{success,message,data:{tables:[...]}} 与 allData"
"allData 为二维数组:首行是列名表头(allData[0])、数据行从 allData[1] 起,"
"每行按 columns 顺序给出全部列的值,行宽 = 列总数(不裁剪自增列)。"
"缺表头时工具会据列名自动补。也接受调用方已组装好的最终结构。"
),
},
},
"required": ["connectionId", "databaseName", "data"],
}
@staticmethod
def _build_body(data: dict, database_name) -> dict:
"""把 preview 返回的 data 组装成 confirm 要求的 body。
后端真实结构(已真机探测确认):
{ "tableStructure": <单表对象,含 columns 且需带 databaseName>, "allData": [...] }
preview 返回的 data.tableStructure 是 {success,message,data:{tables:[...]}} 的包装,
需取出 data.tables[0] 作为单表对象。
"""
if not isinstance(data, dict):
return data
# 若传入的是 preview 工具的完整返回信封 {code, msg, data:{...}},先剥一层
if "tableStructure" not in data and isinstance(data.get("data"), dict):
inner_env = data["data"]
if "tableStructure" in inner_env or "allData" in inner_env:
data = inner_env
ts = data.get("tableStructure")
single_table = None
ts_inner = {}
if isinstance(ts, dict):
if "columns" in ts:
# 已是单表对象(调用方自行组装过)
single_table = dict(ts)
else:
# preview 包装tableStructure.data.tables[0]
ts_inner = ts.get("data") if isinstance(ts.get("data"), dict) else {}
tables = ts_inner.get("tables") if isinstance(ts_inner, dict) else None
if isinstance(tables, list) and tables:
single_table = dict(tables[0])
if single_table is None:
# 无法识别结构,原样透传(兼容调用方已构造好完整 body 的情况)
return data
if database_name and not single_table.get("databaseName"):
single_table["databaseName"] = database_name
# allData 可能落在多个层级(取决于调用方/preview 的嵌套方式),按优先级查找:
# 1. data.allData —— 与 tableStructure 平级(约定的标准位置)
# 2. tableStructure.allData —— 嵌在 tableStructure 包装内(真机/AI 常见误放)
# 3. tableStructure.data.allData —— 嵌在内层 data 里
# 注意:只接受 list任何非 list如内层 data 的 tables 包装对象)都视为未命中,
# 避免把 dict 误当成行数据传给后端。
all_data = None
for candidate in (
data.get("allData"),
ts.get("allData") if isinstance(ts, dict) else None,
ts_inner.get("allData") if isinstance(ts_inner, dict) else None,
):
if isinstance(candidate, list):
all_data = candidate
break
if all_data is None:
all_data = []
# 表头行:后端约定 allData[0] 是「表头行」(列名数组),真实数据从 allData[1] 起
# (见前端 TableRecognition.vue handleComplete 与 CustomizeDbTable.vue validateDataColumns
# 若调用方传的 allData 没带表头(首行就是数据),后端会把首行数据当成字段名,
# 报「查询字段不存在/字段名称不正确」。这里据列名补出表头行。
all_data = ConfirmImportDataTool._ensure_header(single_table.get("columns"), all_data)
return {"tableStructure": single_table, "allData": all_data}
@staticmethod
def _column_names(columns):
"""从列定义中按顺序提取列名数组。"""
if not isinstance(columns, list):
return []
return [c.get("columnName") for c in columns if isinstance(c, dict) and c.get("columnName")]
@staticmethod
def _ensure_header(columns, all_data):
"""确保 allData[0] 是「表头行」(列名数组)。
后端约定allData[0] 为表头(列名),真实数据行从 allData[1] 起;数据行按 columns
顺序给出【全部列】的值(不裁剪自增列)。前端 TableRecognition.vue 在提交前总会把
列名作为首行 push 进 allData。若调用方含 AI传来的 allData 首行已经是数据(缺表头)
后端会把首行当列名解析,报「查询字段不存在/字段名称不正确」。这里据列名补表头:
- 首行恰好等于列名数组 → 视为已带表头,原样返回
- 否则 → 在最前面补一行列名
"""
names = ConfirmImportDataTool._column_names(columns)
if not names or not isinstance(all_data, list) or not all_data:
return all_data
first = all_data[0]
if isinstance(first, list) and list(first) == names:
return all_data # 已带表头
return [names, *all_data]
async def execute(self, args: dict) -> dict:
args = dict(args)
connection_id = args.pop("connectionId")
target = args.pop("target", None)
database_name = args.pop("databaseName", None)
data = args.pop("data")
# 容错databaseName / target 可能被放进 data 里AI 常把 preview 返回的整块连同
# databaseName/target 一起塞进 data。顶层没给时从 data 里回捞,并清出 data
# 避免污染最终 body。
if isinstance(data, dict):
if database_name is None and data.get("databaseName"):
database_name = data.get("databaseName")
if target is None and data.get("target"):
target = data.get("target")
data = {k: v for k, v in data.items() if k not in ("databaseName", "target")}
if target is None:
target = "test"
body = self._build_body(data, database_name)
# 预检:把后端那两个含糊的报错(「导入数据不能为空」/「数据库名称不能为空」)
# 提前在工具层拦下,给出可操作的提示(指明 allData/databaseName 该放哪),
# 避免调用方对着后端原文反复试错。仅在 body 已被识别为标准结构时校验。
if isinstance(body, dict) and "tableStructure" in body:
if not body.get("allData"):
raise ValueError(
"导入数据为空:未能从 data 中解析到 allData数据行"
"请确认 allData 是一个非空数组,可放在 data.allData、"
"data.tableStructure.allData 或 data.tableStructure.data.allData 任一层级。"
)
ts = body["tableStructure"]
if isinstance(ts, dict) and not ts.get("databaseName"):
raise ValueError(
"缺少 databaseName落库目标库名请通过顶层参数 databaseName 传入,"
"或放在 data.databaseName 中(工具会自动塞进表对象)。"
)
# 行宽与表头校验_ensure_header 已保证 allData[0] 是表头行(列名)。
# 后端要求每行(含表头)宽度 = 列数(全部列,含自增列占位),且表头之外至少有 1 行数据。
# 行宽对不上后端只会回含糊的「字段名称不正确/查询字段不存在」,这里提前报清楚。
cols = ts.get("columns") if isinstance(ts, dict) else None
all_data = body["allData"]
if isinstance(cols, list) and cols:
total = len(cols)
# 表头之外至少要有一行真实数据
data_rows = [r for r in all_data[1:] if isinstance(r, list)]
if not data_rows:
raise ValueError(
"导入数据为空allData 除表头行外没有任何数据行。"
"allData 约定首行为表头(列名),真实数据从第 2 行起。"
)
for idx, row in enumerate(all_data):
if isinstance(row, list) and len(row) != total:
raise ValueError(
f"{idx + 1} 行列数为 {len(row)},与表结构的 {total} 列不匹配。"
"allData 每行(含表头行)都应按 columns 顺序给出全部列的值;"
"首行须为列名表头,数据行从第 2 行起。请核对是否多/少了列。"
)
return await self.client.post(
f"/datasource/connection/{connection_id}/import_document/confirm",
json_data=body,
params={"target": target},
)

View File

@@ -0,0 +1,286 @@
"""
数据库与表管理工具 (工具 7-12)
"""
from ._base import register_tool, ToolDef
@register_tool("list_databases")
class ListDatabasesTool(ToolDef):
name = "list_databases"
description = "获取指定数据源下的数据库列表"
input_schema = {
"type": "object",
"properties": {
"datasourceId": {"type": "string", "description": "数据源 ID"},
"sourceType": {"type": "string", "enum": ["builtin", "external"], "description": "数据源类型"},
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
},
"required": ["datasourceId"],
}
async def execute(self, args: dict) -> dict:
params = {k: v for k, v in args.items() if v is not None}
return await self.client.get("/datasource/config/list", params=params)
@register_tool("list_tables")
class ListTablesTool(ToolDef):
name = "list_tables"
description = "获取指定数据源下的表列表"
input_schema = {
"type": "object",
"properties": {
"datasourceId": {"type": "string", "description": "数据源 ID"},
"sourceType": {"type": "string", "enum": ["builtin", "external"], "description": "数据源类型"},
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
},
"required": ["datasourceId"],
}
async def execute(self, args: dict) -> dict:
params = {k: v for k, v in args.items() if v is not None}
return await self.client.get("/datasource/table/list", params=params)
@register_tool("list_tables_with_ai")
class ListTablesWithAiTool(ToolDef):
name = "list_tables_with_ai"
description = (
"获取表元数据列表(含 AI 训练描述),内置库管理的主列表。"
"注意datasourceId 实为「配置/库」ID即 create_connection_config 落库后的 config id"
)
input_schema = {
"type": "object",
"properties": {
"datasourceId": {"type": "string", "description": "配置/库 ID非数据源连接 ID"},
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
},
"required": ["datasourceId"],
}
async def execute(self, args: dict) -> dict:
params = {k: v for k, v in args.items() if v is not None}
return await self.client.get("/datasource/table/ailist", params=params)
@register_tool("get_table_detail")
class GetTableDetailTool(ToolDef):
name = "get_table_detail"
description = "获取表的完整结构信息(字段列表、主键、类型等)"
input_schema = {
"type": "object",
"properties": {
"tableId": {"type": "string", "description": "表 ID"},
},
"required": ["tableId"],
}
async def execute(self, args: dict) -> dict:
table_id = args["tableId"]
return await self.client.get(f"/datasource/table/{table_id}/detail")
@register_tool("create_table")
class CreateTableTool(ToolDef):
name = "create_table"
description = "在指定数据库创建新表"
input_schema = {
"type": "object",
"properties": {
"connectionId": {"type": "string", "description": "数据源连接 ID"},
"databaseName": {"type": "string", "description": "目标数据库名"},
"tableName": {"type": "string", "description": "表名(小写字母+数字+下划线)"},
"tableComment": {"type": "string", "description": "表注释"},
"columns": {
"type": "array",
"description": "字段定义数组",
"items": {
"type": "object",
"properties": {
"columnName": {"type": "string", "description": "字段名"},
"columnType": {"type": "string", "description": "字段类型VARCHAR/INTEGER/SERIAL等"},
"columnLength": {"type": "integer", "description": "字段长度"},
"isPrimaryKey": {"type": "boolean", "description": "是否主键"},
"isNullable": {"type": "boolean", "description": "是否可空"},
"isAutoIncrement": {"type": "boolean", "description": "是否自增"},
"columnComment": {"type": "string", "description": "字段注释"},
"defaultValue": {"type": "string", "description": "默认值"},
},
"required": ["columnName", "columnType"],
},
},
},
"required": ["connectionId", "databaseName", "tableName", "columns"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
connection_id = args.pop("connectionId")
return await self.client.post(f"/datasource/connection/{connection_id}/create_table", json_data=args)
@register_tool("alter_table")
class AlterTableTool(ToolDef):
name = "alter_table"
description = "修改已有表结构(增/改/删字段)"
input_schema = {
"type": "object",
"properties": {
"connectionId": {"type": "string", "description": "数据源连接 ID"},
"databaseName": {"type": "string", "description": "数据库名"},
"tableName": {"type": "string", "description": "表名"},
"operations": {
"type": "array",
"description": "表结构变更操作数组",
"items": {
"type": "object",
"properties": {
"operation": {"type": "string", "enum": ["ADD_COLUMN", "MODIFY_COLUMN", "DROP_COLUMN"], "description": "变更类型:新增/修改/删除字段(后端实测仅支持这三种)"},
"column": {"type": "object", "description": "列定义。ADD/MODIFY 需 columnName+columnType(+columnLength/columnComment 等)DROP 仅需 columnName"},
},
"required": ["operation", "column"],
},
},
"tableComment": {"type": "string", "description": "表注释"},
},
"required": ["connectionId", "databaseName", "tableName", "operations"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
connection_id = args.pop("connectionId")
return await self.client.put(f"/datasource/connection/{connection_id}/alter_table", json_data=args)
@register_tool("generate_table_by_description")
class GenerateTableByDescriptionTool(ToolDef):
name = "generate_table_by_description"
description = "通过自然语言描述让 AI 生成表结构(异步任务,返回 taskId。需配合 get_ai_training_detail 轮询 taskId 获取生成结果"
input_schema = {
"type": "object",
"properties": {
"requirement": {"type": "string", "description": "业务需求描述"},
"databaseId": {"type": "integer", "description": "关联的数据库 ID可选"},
},
"required": ["requirement"],
}
async def execute(self, args: dict) -> dict:
return await self.client.post("/datasource/connection/generate_table", json_data=args)
@register_tool("create_database")
class CreateDatabaseTool(ToolDef):
name = "create_database"
description = "在内置数据源下创建一个数据库(建表前置步骤)"
input_schema = {
"type": "object",
"properties": {
"connectionId": {"type": "string", "description": "数据源连接 ID"},
"databaseName": {"type": "string", "description": "数据库名(仅字母数字下划线)"},
},
"required": ["connectionId", "databaseName"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
connection_id = args.pop("connectionId")
return await self.client.post(f"/datasource/connection/{connection_id}/create_database", json_data=args)
@register_tool("create_database_table")
class CreateDatabaseTableTool(ToolDef):
name = "create_database_table"
description = (
"一次性创建数据库 + 表(合并接口,免去先建库再建表)。"
"支持两种用法:①单表便捷参数 tableName/tableComment/columns"
"②批量传 tables 数组(一次建多张表)。二者任选其一"
)
# 单字段columnName/columnType/...)的 schema单表和批量两种用法共用
_COLUMN_ITEMS = {
"type": "object",
"properties": {
"columnName": {"type": "string", "description": "字段名"},
"columnType": {"type": "string", "description": "字段类型VARCHAR/INTEGER/SERIAL等"},
"columnLength": {"type": "integer", "description": "字段长度"},
"isPrimaryKey": {"type": "boolean", "description": "是否主键"},
"isNullable": {"type": "boolean", "description": "是否可空"},
"isAutoIncrement": {"type": "boolean", "description": "是否自增"},
"columnComment": {"type": "string", "description": "字段注释"},
"defaultValue": {"type": "string", "description": "默认值"},
},
"required": ["columnName", "columnType"],
}
input_schema = {
"type": "object",
"properties": {
"connectionId": {"type": "string", "description": "数据源连接 ID"},
"databaseName": {"type": "string", "description": "数据库名(仅字母数字下划线)"},
# —— 用法①:单表便捷参数 ——
"tableName": {"type": "string", "description": "表名(用法①单表,小写字母+数字+下划线)"},
"tableComment": {"type": "string", "description": "表注释(用法①单表)"},
"columns": {
"type": "array",
"description": "字段定义数组(用法①单表,结构同 create_table",
"items": _COLUMN_ITEMS,
},
# —— 用法②:批量 tables 数组 ——
"tables": {
"type": "array",
"description": "表定义数组(用法②批量建多表),每项含 tableName/tableComment/columns",
"items": {
"type": "object",
"properties": {
"tableName": {"type": "string", "description": "表名"},
"tableComment": {"type": "string", "description": "表注释"},
"columns": {"type": "array", "description": "字段定义数组", "items": _COLUMN_ITEMS},
},
"required": ["tableName", "columns"],
},
},
},
"required": ["connectionId", "databaseName"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
connection_id = args.pop("connectionId")
# 后端真实 body 结构:{databaseName, tables:[{tableName,tableComment,columns}]}(已真机探测确认)
if not args.get("tables"):
table = {"tableName": args.pop("tableName", None), "columns": args.pop("columns", [])}
if args.get("tableComment") is not None:
table["tableComment"] = args.pop("tableComment")
args["tables"] = [table]
else:
# 批量用法下,清掉可能并存的单表字段,避免歧义
args.pop("tableName", None)
args.pop("tableComment", None)
args.pop("columns", None)
return await self.client.post(f"/datasource/connection/{connection_id}/create_database_table", json_data=args)
@register_tool("alter_database")
class AlterDatabaseTool(ToolDef):
name = "alter_database"
description = "修改数据库(如改名)"
input_schema = {
"type": "object",
"properties": {
"connectionId": {"type": "string", "description": "数据源连接 ID"},
"databaseName": {"type": "string", "description": "当前数据库名"},
"newName": {"type": "string", "description": "新数据库名"},
},
"required": ["connectionId", "databaseName", "newName"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
connection_id = args.pop("connectionId")
# 后端改名字段名为 newName已真机探测确认兼容历史传参 newDatabaseName
if "newName" not in args and "newDatabaseName" in args:
args["newName"] = args.pop("newDatabaseName")
return await self.client.put(f"/datasource/connection/{connection_id}/alter_database", json_data=args)

View File

@@ -0,0 +1,249 @@
"""
数据源管理工具 (工具 1-6)
"""
import logging
from ._base import register_tool, ToolDef
logger = logging.getLogger(__name__)
@register_tool("list_datasources")
class ListDatasourcesTool(ToolDef):
name = "list_datasources"
description = "获取数据源列表,支持搜索和状态筛选"
input_schema = {
"type": "object",
"properties": {
"datasourceName": {"type": "string", "description": "数据源名称模糊搜索"},
"status": {"type": "integer", "description": "0=运行中, 1=已停止, 不传=全部"},
"sourceType": {"type": "string", "enum": ["builtin", "external"], "description": "数据源类型"},
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
},
"required": [],
}
async def execute(self, args: dict) -> dict:
params = {k: v for k, v in args.items() if v is not None}
return await self.client.get("/datasource/connection/list", params=params)
@register_tool("get_datasource_detail")
class GetDatasourceDetailTool(ToolDef):
name = "get_datasource_detail"
description = "获取单个数据源的完整详情(含配置和实时结构)"
input_schema = {
"type": "object",
"properties": {
"datasourceId": {"type": "string", "description": "数据源 ID"},
},
"required": ["datasourceId"],
}
async def execute(self, args: dict) -> dict:
ds_id = args["datasourceId"]
result = {}
# 获取基本信息
try:
result["detail"] = await self.client.get(f"/datasource/connection/{ds_id}")
except Exception as e:
result["detail"] = {"error": str(e)}
# 获取配置
try:
result["config"] = await self.client.get(f"/datasource/config/{ds_id}")
except Exception as e:
result["config"] = {"error": str(e)}
# 获取实时结构
try:
result["structure"] = await self.client.get(f"/datasource/connection/realtime/structure/{ds_id}")
except Exception as e:
result["structure"] = {"error": str(e)}
return result
@register_tool("create_datasource")
class CreateDatasourceTool(ToolDef):
name = "create_datasource"
description = "创建外部数据源连接(可选先测试连接)"
input_schema = {
"type": "object",
"properties": {
"datasourceName": {"type": "string", "description": "数据源名称3-20字"},
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
"host": {"type": "string", "description": "数据库地址"},
"port": {"type": "integer", "description": "端口号"},
"databaseName": {"type": "string", "description": "要连接的数据库名"},
"username": {"type": "string", "description": "数据库用户名"},
"password": {"type": "string", "description": "密码"},
"remark": {"type": "string", "description": "数据源描述"},
"connectionType": {"type": "string", "enum": ["user_password", "ssl"], "description": "连接类型,默认 user_password"},
"test_first": {"type": "boolean", "default": True, "description": "是否先测试连接,默认 true"},
},
"required": ["datasourceName", "datasourceType", "host", "port", "databaseName", "username"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
test_first = args.pop("test_first", True)
# 如果需要先测试连接(测试失败时 client 会直接抛异常,由上层统一捕获)
if test_first:
test_data = {
"datasourceName": args.get("datasourceName"),
"datasourceType": args.get("datasourceType"),
"host": args.get("host"),
"port": args.get("port"),
"databaseName": args.get("databaseName"),
"username": args.get("username"),
"password": args.get("password"),
"connectionType": args.get("connectionType", "user_password"),
}
await self.client.post("/datasource/connection/test", json_data=test_data)
# 创建数据源
return await self.client.post("/datasource/connection", json_data=args)
@register_tool("update_datasource")
class UpdateDatasourceTool(ToolDef):
name = "update_datasource"
description = "更新数据源连接信息"
input_schema = {
"type": "object",
"properties": {
"id": {"type": "string", "description": "数据源 ID"},
"datasourceName": {"type": "string", "description": "更新名称"},
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
"host": {"type": "string", "description": "更新地址"},
"port": {"type": "integer", "description": "更新端口"},
"databaseName": {"type": "string", "description": "更新数据库名"},
"username": {"type": "string", "description": "更新用户名"},
"password": {"type": "string", "description": "新密码(不传则不变)"},
"connectionType": {"type": "string", "enum": ["user_password", "ssl"], "description": "连接类型"},
"remark": {"type": "string", "description": "更新描述"},
},
"required": ["id"],
}
async def execute(self, args: dict) -> dict:
return await self.client.put("/datasource/connection", json_data=args)
@register_tool("toggle_datasource_status")
class ToggleDatasourceStatusTool(ToolDef):
name = "toggle_datasource_status"
description = "启用/停用数据源"
input_schema = {
"type": "object",
"properties": {
"id": {"type": "string", "description": "数据源 ID"},
"status": {"type": "integer", "enum": [0, 1], "description": "0=启用, 1=停用"},
},
"required": ["id", "status"],
}
async def execute(self, args: dict) -> dict:
return await self.client.put("/datasource/connection/changeStatus", json_data=args)
@register_tool("delete_datasource")
class DeleteDatasourceTool(ToolDef):
name = "delete_datasource"
description = "删除数据源(运行中会自动先停用)"
input_schema = {
"type": "object",
"properties": {
"id": {"type": "string", "description": "数据源 ID"},
},
"required": ["id"],
}
async def execute(self, args: dict) -> dict:
ds_id = args["id"]
# 先尝试停用(仅忽略已停用等预期错误)
try:
await self.client.put("/datasource/connection/changeStatus", json_data={"id": ds_id, "status": 1})
except Exception as e:
# 记录日志但继续删除
logger.debug(f"停用数据源失败(可能已停用): {e}")
# 删除数据源
return await self.client.delete(f"/datasource/connection/{ds_id}")
@register_tool("test_connection")
class TestConnectionTool(ToolDef):
name = "test_connection"
description = "测试外部数据库连接是否可用(不创建数据源)"
input_schema = {
"type": "object",
"properties": {
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
"host": {"type": "string", "description": "数据库地址"},
"port": {"type": "integer", "description": "端口号"},
"databaseName": {"type": "string", "description": "要连接的数据库名"},
"username": {"type": "string", "description": "数据库用户名"},
"password": {"type": "string", "description": "密码"},
"connectionType": {"type": "string", "enum": ["user_password", "ssl"], "description": "连接类型,默认 user_password"},
},
"required": ["datasourceType", "host", "port", "databaseName", "username"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
args.setdefault("connectionType", "user_password")
return await self.client.post("/datasource/connection/test", json_data=args)
@register_tool("get_realtime_structure")
class GetRealtimeStructureTool(ToolDef):
name = "get_realtime_structure"
description = "拉取数据源真实的库表结构(实时探测远端数据库)"
input_schema = {
"type": "object",
"properties": {
"datasourceId": {"type": "string", "description": "数据源 ID"},
},
"required": ["datasourceId"],
}
async def execute(self, args: dict) -> dict:
ds_id = args["datasourceId"]
return await self.client.get(f"/datasource/connection/realtime/structure/{ds_id}")
@register_tool("create_builtin_datasource")
class CreateBuiltinDatasourceTool(ToolDef):
name = "create_builtin_datasource"
description = "创建内置 PostgreSQL 数据源连接,返回 connectionId内置源建库建表链路第一步"
input_schema = {
"type": "object",
"properties": {
"datasourceName": {"type": "string", "description": "数据源名称3-20字"},
"remark": {"type": "string", "description": "数据源描述"},
},
"required": ["datasourceName"],
}
async def execute(self, args: dict) -> dict:
return await self.client.post("/datasource/connection/create_builtin_postgresql", json_data=args)
@register_tool("update_builtin_datasource")
class UpdateBuiltinDatasourceTool(ToolDef):
name = "update_builtin_datasource"
description = "修改内置 PostgreSQL 数据源连接信息"
input_schema = {
"type": "object",
"properties": {
"id": {"type": "string", "description": "数据源 ID"},
"datasourceName": {"type": "string", "description": "更新名称"},
"remark": {"type": "string", "description": "更新描述"},
},
"required": ["id"],
}
async def execute(self, args: dict) -> dict:
return await self.client.put("/datasource/connection/update_builtin_database", json_data=args)

View File

@@ -0,0 +1,151 @@
"""
MQTT 字段关联同步工具(内置源专属,文档 §8
接口前缀为 /system/mqtt注意与 /datasource 不同)。
关于 sourceTable / targetTable 的方向(已真机验证):
直接调后端 API 时按字面透传即可——create 传什么detail 回显就是什么,
字段自洽、后端不做调换。例create 传 sourceTable=sys_user/targetTable=users
detail 读回仍是 sourceTable=sys_user/targetTable=users。
文档 §8 提到的「前端提交时调换」是前端 Vue 组件为 UI 展示所做的转换,
与后端 API 契约无关,本工具(直连 API无需调换。
"""
from ._base import register_tool, ToolDef
@register_tool("list_mqtt_configs")
class ListMqttConfigsTool(ToolDef):
name = "list_mqtt_configs"
description = "获取 MQTT 字段关联同步配置列表"
input_schema = {
"type": "object",
"properties": {
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
},
"required": [],
}
async def execute(self, args: dict) -> dict:
params = {k: v for k, v in args.items() if v is not None}
return await self.client.get("/system/mqtt/config/list", params=params)
@register_tool("get_mqtt_config_detail")
class GetMqttConfigDetailTool(ToolDef):
name = "get_mqtt_config_detail"
description = "获取 MQTT 同步配置详情"
input_schema = {
"type": "object",
"properties": {
"configId": {"type": "string", "description": "配置 ID"},
},
"required": ["configId"],
}
async def execute(self, args: dict) -> dict:
return await self.client.get(f"/system/mqtt/config/{args['configId']}")
@register_tool("list_mqtt_target_tables")
class ListMqttTargetTablesTool(ToolDef):
name = "list_mqtt_target_tables"
description = "获取 MQTT 可同步的目标表列表"
input_schema = {"type": "object", "properties": {}, "required": []}
async def execute(self, args: dict) -> dict:
return await self.client.get("/system/mqtt/config/tableList")
@register_tool("list_mqtt_target_table_columns")
class ListMqttTargetTableColumnsTool(ToolDef):
name = "list_mqtt_target_table_columns"
description = "获取 MQTT 目标表的字段列表"
input_schema = {
"type": "object",
"properties": {
"tableName": {"type": "string", "description": "目标表名"},
},
"required": ["tableName"],
}
async def execute(self, args: dict) -> dict:
return await self.client.get(f"/system/mqtt/config/tableColumns/{args['tableName']}")
@register_tool("create_mqtt_config")
class CreateMqttConfigTool(ToolDef):
name = "create_mqtt_config"
description = (
"新增 MQTT 字段关联同步配置。"
"sourceTable/targetTable 按字面透传即可(实测确认 create 存入与 detail 回显一致,"
"无需调换文档§8 的调换是前端 UI 层行为,与本 API 无关)"
)
input_schema = {
"type": "object",
"properties": {
"sourceTable": {"type": "string", "description": "源表名"},
"targetTable": {"type": "string", "description": "目标表名"},
"config": {"type": "object", "description": "完整配置体(字段映射等),按后端结构透传"},
},
"required": [],
}
async def execute(self, args: dict) -> dict:
# config 若存在则以其为主体,否则直接用 args去掉 config 包裹键)
body = args.get("config") if isinstance(args.get("config"), dict) else dict(args)
return await self.client.post("/system/mqtt/config", json_data=body)
@register_tool("update_mqtt_config")
class UpdateMqttConfigTool(ToolDef):
name = "update_mqtt_config"
description = (
"修改 MQTT 字段关联同步配置。"
"sourceTable/targetTable 按字面透传即可(实测确认无需调换,详见 create_mqtt_config"
)
input_schema = {
"type": "object",
"properties": {
"id": {"type": "string", "description": "配置 ID"},
"sourceTable": {"type": "string", "description": "源表名"},
"targetTable": {"type": "string", "description": "目标表名"},
"config": {"type": "object", "description": "完整配置体,按后端结构透传"},
},
"required": ["id"],
}
async def execute(self, args: dict) -> dict:
if isinstance(args.get("config"), dict):
body = dict(args["config"])
body.setdefault("id", args.get("id"))
else:
body = dict(args)
return await self.client.put("/system/mqtt/config", json_data=body)
@register_tool("delete_mqtt_config")
class DeleteMqttConfigTool(ToolDef):
name = "delete_mqtt_config"
description = "删除 MQTT 同步配置(支持多个 id逗号拼接"
input_schema = {
"type": "object",
"properties": {
"ids": {"type": "string", "description": "配置 ID多个用逗号拼接"},
},
"required": ["ids"],
}
async def execute(self, args: dict) -> dict:
return await self.client.delete(f"/system/mqtt/config/{args['ids']}")
@register_tool("refresh_mqtt_config_cache")
class RefreshMqttConfigCacheTool(ToolDef):
name = "refresh_mqtt_config_cache"
description = "刷新 MQTT 同步配置缓存"
input_schema = {"type": "object", "properties": {}, "required": []}
async def execute(self, args: dict) -> dict:
return await self.client.get("/system/mqtt/config/refreshCache")

View File

@@ -0,0 +1,386 @@
"""
技能与工具管理工具
把 SQL 沉淀为数据源可复用工具的入口统一走 add_sql_tool_to_datasource保证技能必有工具
内部按需建技能+写配置+建工具)。其余为底层积木:查询类、向已有技能加/改工具、改技能配置。
不单独暴露「只建技能」工具,避免产生无效空技能。
"""
import json
from ._base import register_tool, ToolDef
@register_tool("get_skill_by_datasource")
class GetSkillByDatasourceTool(ToolDef):
name = "get_skill_by_datasource"
description = "根据数据源获取技能信息"
input_schema = {
"type": "object",
"properties": {
"datasourceId": {"type": "string", "description": "数据源 ID"},
},
"required": ["datasourceId"],
}
async def execute(self, args: dict) -> dict:
return await self.client.get(f"/datasource/skill/getByDatasource/{args['datasourceId']}")
@register_tool("get_skill_tools")
class GetSkillToolsTool(ToolDef):
name = "get_skill_tools"
description = "获取技能下的工具列表"
input_schema = {
"type": "object",
"properties": {
"skillId": {"type": "string", "description": "技能 ID"},
},
"required": ["skillId"],
}
async def execute(self, args: dict) -> dict:
return await self.client.get(f"/datasource/skill/getBySkillId/{args['skillId']}")
@register_tool("create_sql_tool")
class CreateSqlToolTool(ToolDef):
name = "create_sql_tool"
description = "将 SQL 查询创建为可复用工具(支持批量)"
input_schema = {
"type": "object",
"properties": {
"skillId": {"type": "string", "description": "技能 ID"},
"tableIds": {
"type": "array",
"description": "关联的表 ID 数组",
"items": {"type": "string"},
},
"suggestions": {
"type": "array",
"description": "SQL 工具建议数组",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "工具名称"},
"businessDescription": {"type": "string", "description": "业务描述"},
"sqlTemplate": {"type": "string", "description": "SQL 模板(支持 #{param} 参数占位)"},
"sqlParams": {"type": "string", "description": "参数定义,最终以 JSON 字符串提交(传 dict 会自动 json.dumps。内容形态后端不挑剔可为 JSON Schema 对象串如 {\"type\":\"object\",...},也可为字段定义数组串;无参数传 \"{}\""},
"resultType": {"type": "string", "enum": ["single", "list"], "default": "list", "description": "结果类型,默认 list"},
"businessScenario": {"type": "string", "description": "业务场景描述"},
},
"required": ["name", "businessDescription", "sqlTemplate"],
},
},
},
"required": ["skillId", "suggestions"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
# tableIds 后端始终期望该键存在:前端真机始终传 ""(空串)。
# 调用方未给时补 "",与前端 postSqlSkillConfirmTools 的请求保持一致。
if "tableIds" not in args or args["tableIds"] is None:
args["tableIds"] = ""
# 处理 suggestions 中的 sqlParamsdict 自动序列化为 JSON 字符串;
# 同时补齐 resultType 默认值 list与前端默认一致
if "suggestions" in args and isinstance(args["suggestions"], list):
for suggestion in args["suggestions"]:
if "sqlParams" in suggestion and isinstance(suggestion["sqlParams"], dict):
suggestion["sqlParams"] = json.dumps(suggestion["sqlParams"])
suggestion.setdefault("resultType", "list")
return await self.client.post("/datasource/skill/confirmTools", json_data=args)
@register_tool("delete_skill_tool")
class DeleteSkillToolTool(ToolDef):
name = "delete_skill_tool"
description = "删除技能下的工具"
input_schema = {
"type": "object",
"properties": {
"skillToolId": {"type": "string", "description": "工具 ID"},
},
"required": ["skillToolId"],
}
async def execute(self, args: dict) -> dict:
return await self.client.delete(f"/datasource/skill/tskilltool/{args['skillToolId']}")
@register_tool("update_skill_config")
class UpdateSkillConfigTool(ToolDef):
name = "update_skill_config"
description = (
"更新技能(名称/描述/MCP 配置模板等)。对应后端 skill/updateOrGet。"
"datasourceId 与 skillId 均必填且为真实 ID来自其他工具返回不可臆造"
"若不显式传 configTemplate会按这两个 ID 自动生成 lzwcai-mcp-sqlexecutor 的标准 MCP 配置模板(与前端一致)。"
)
input_schema = {
"type": "object",
"properties": {
"datasourceId": {
"type": "string",
"description": "数据源/配置 ID真实 ID来自 list_databases / list_tables_with_ai / get_connection_config_list不可臆造",
},
"skillId": {
"type": "string",
"description": "技能 ID真实 ID来自 get_skill_by_datasource 的返回,不可臆造;与 datasourceId 一起用于生成 configTemplate",
},
"name": {"type": "string", "description": "技能名称(可选)"},
"description": {"type": "string", "description": "技能描述(可选)"},
"configTemplate": {
"type": "string",
"description": "配置模板 JSON 字符串(可选)。不传时按 datasourceId + skillId 自动生成标准模板",
},
},
"required": ["datasourceId", "skillId"],
}
@staticmethod
def _build_config_template(datasource_id: str, skill_id: str) -> str:
"""生成 lzwcai-mcp-sqlexecutor 的标准 MCP 配置模板。
模板大部分是固定值,仅 mcpServerKey 后缀、env.databaseId、env.skillId 随
datasourceId / skillId 动态变化(与前端 SqlControllerMsg.vue 的 configTemplateObj 完全一致)。
"""
mcp_server_key = f"lzwcai_mcp_sqlexecutor_{datasource_id}"
config_obj = {
"mcpServers": {
mcp_server_key: {
"command": "uvx",
"type": "stdio",
"args": ["lzwcai-mcp-sqlexecutor"],
"tiemout": 200,
"env": {
"databaseId": datasource_id,
"skillId": skill_id,
},
}
}
}
return json.dumps(config_obj)
async def execute(self, args: dict) -> dict:
args = dict(args)
# 如果 configTemplate 是 dict转为 JSON 字符串
if "configTemplate" in args and isinstance(args["configTemplate"], dict):
args["configTemplate"] = json.dumps(args["configTemplate"])
# 未显式提供 configTemplate 时,按 datasourceId + skillId 自动生成标准模板
elif not args.get("configTemplate"):
args["configTemplate"] = self._build_config_template(
str(args["datasourceId"]), str(args["skillId"])
)
return await self.client.post("/datasource/skill/updateOrGet", json_data=args)
@register_tool("update_skill_tool")
class UpdateSkillToolTool(ToolDef):
name = "update_skill_tool"
description = (
"修改技能下某个工具的名称/描述/SQL等对应后端 tskilltool/updateOrGetupsert 语义)。"
"改展示名传 name 即可(工具会同时写 name 和 uniqueName 两个字段,与工具实体存储一致)。"
"工具名建议遵循前端约束≤20 字、只含中英文/数字/空格、不含特殊符号。"
)
# 工具实体同时存在 name 与 uniqueName 两个字段(真机数据里二者取值相同,见 skill.json
# 前端 ChatDebugging.vue handleEditToolSubmit 改名时提交 {id, name, description}(用 name
# 早期真机探测又得到 uniqueName。为消除歧义、与实体存储保持一致改名时两个字段都写。
input_schema = {
"type": "object",
"properties": {
"id": {"type": "string", "description": "技能工具 IDget_skill_tools 返回的 id"},
"name": {"type": "string", "description": "工具展示名(可选)。会同时写入 name 与 uniqueName"},
"description": {"type": "string", "description": "工具描述(可选)"},
"sqlTemplate": {"type": "string", "description": "SQL 模板(可选)"},
"resultType": {"type": "string", "enum": ["single", "list"], "description": "结果类型(可选)"},
},
"required": ["id"],
}
# 旧参数名 -> 真实字段名(向后兼容此前错误实现的调用方)
_LEGACY_MAP = {
"skillToolId": "id",
"businessDescription": "description",
}
async def execute(self, args: dict) -> dict:
args = dict(args)
for old, new in self._LEGACY_MAP.items():
if old in args and new not in args:
args[new] = args.pop(old)
else:
args.pop(old, None)
# 展示名name / uniqueName 任一传入都同步到两个字段(与工具实体存储一致,
# 兼容前端用 name、早期探测用 uniqueName 两种契约,避免改名不生效)。
display_name = args.get("name") if args.get("name") is not None else args.get("uniqueName")
if display_name is not None:
args["name"] = display_name
args["uniqueName"] = display_name
# businessScenario 后端实体无此字段,丢弃避免干扰
args.pop("businessScenario", None)
return await self.client.post("/datasource/skill/tskilltool/updateOrGet", json_data=args)
@register_tool("add_sql_tool_to_datasource")
class AddSqlToolToDatasourceTool(ToolDef):
name = "add_sql_tool_to_datasource"
description = (
"把一条 SQL 沉淀为数据源的可复用工具(一步到位,推荐用这个而不是手动拼 "
"update_skill_config/create_sql_tool\n"
"【为什么用它】技能(skill)必须挂着工具才有效,单独建技能会留下无效的空技能。本工具内部"
"1:1 复刻前端 handleAddToolSubmit 的完整链路,保证技能必有工具:\n"
" 读 skillBool → 技能不存在则 createOrGet 建技能 → getByDatasource 拿真实 skillId →"
" 按 sqlTemplate 去重 → 技能新建时写 lzwcai-mcp-sqlexecutor 配置模板 → confirmTools 建工具。\n"
"【幂等/去重】同一 datasourceId 下若已存在 sqlTemplate 相同的工具,直接返回 skipped不重复创建。\n"
"datasourceId 必填且为真实 ID来自 list_databases / list_tables_with_ai / get_connection_config_list"
)
input_schema = {
"type": "object",
"properties": {
"datasourceId": {"type": "string", "description": "数据源/配置 ID真实 ID不可臆造"},
"name": {"type": "string", "description": "工具名称(展示名)"},
"businessDescription": {"type": "string", "description": "工具的业务描述"},
"sqlTemplate": {"type": "string", "description": "SQL 模板(支持 #{param} 参数占位)"},
"sqlParams": {
"type": "string",
"description": "参数定义,最终以 JSON 字符串提交(传 dict 会自动 json.dumps。内容形态后端不挑剔JSON Schema 对象串或字段定义数组串均可);不传默认空 schema",
},
"resultType": {
"type": "string",
"enum": ["single", "list"],
"default": "list",
"description": "结果类型,默认 list",
},
"businessScenario": {"type": "string", "description": "业务场景描述(可选)"},
"tableIds": {
"type": "array",
"items": {"type": "string"},
"description": "关联的表 ID 数组(可选)",
},
"skillName": {"type": "string", "description": "技能不存在时新建技能用的名称(可选,不传自动生成)"},
"skillDescription": {"type": "string", "description": "技能不存在时新建技能用的描述(可选)"},
},
"required": ["datasourceId", "name", "businessDescription", "sqlTemplate"],
}
@staticmethod
def _unwrap(resp):
"""{code,msg,data} 信封里取 data非信封则原样返回。"""
if isinstance(resp, dict) and "data" in resp and ("code" in resp or "msg" in resp):
return resp["data"]
return resp
@staticmethod
def _normalize_sql(sql) -> str:
"""归一化 SQL 用于去重比较:折叠空白 + strip与前端 replace(/\\s+/g,' ').trim() 一致)。"""
if not isinstance(sql, str):
return ""
return " ".join(sql.split()).strip()
async def _get_skill_id(self, datasource_id: str):
"""getByDatasource 拿技能 id拿不到返回 None。"""
resp = await self.client.get(f"/datasource/skill/getByDatasource/{datasource_id}")
data = self._unwrap(resp)
if isinstance(data, dict):
return data.get("id")
return None
async def execute(self, args: dict) -> dict:
args = dict(args)
datasource_id = str(args["datasourceId"])
sql_template = args["sqlTemplate"]
# 1. 读数据源配置,判断 skillBool技能是否已存在
config_resp = await self.client.get(f"/datasource/config/{datasource_id}")
config_data = self._unwrap(config_resp)
skill_bool = config_data.get("skillBool") if isinstance(config_data, dict) else None
created_skill = False
# 2. 技能不存在 → 先建技能createOrGet
if skill_bool is not True:
create_skill_body = {"datasourceId": datasource_id}
if args.get("skillName"):
create_skill_body["name"] = args["skillName"]
if args.get("skillDescription"):
create_skill_body["description"] = args["skillDescription"]
await self.client.post("/datasource/skill/createOrGet", json_data=create_skill_body)
created_skill = True
# 3. 拿真实 skillId注意id 来自 getByDatasource不是 createOrGet 的返回)
skill_id = await self._get_skill_id(datasource_id)
if not skill_id:
raise ValueError(
f"未能获取数据源 {datasource_id} 的技能 IDgetByDatasource 未返回 id"
"请确认 datasourceId 正确、技能创建是否成功。"
)
skill_id = str(skill_id)
# 4. 去重:同 sqlTemplate 的工具已存在则跳过
tools_resp = await self.client.get(f"/datasource/skill/getBySkillId/{skill_id}")
tools_data = self._unwrap(tools_resp)
target_norm = self._normalize_sql(sql_template)
if isinstance(tools_data, list):
for tool in tools_data:
if isinstance(tool, dict) and self._normalize_sql(tool.get("sqlTemplate")) == target_norm:
return {
"skipped": True,
"reason": "已存在 sqlTemplate 相同的工具,未重复创建",
"skillId": skill_id,
"existingTool": {
"id": tool.get("id"),
"uniqueName": tool.get("uniqueName") or tool.get("name"),
},
}
# 5. 技能是本次新建的 → 写入 lzwcai-mcp-sqlexecutor 标准配置模板
if created_skill:
config_template = UpdateSkillConfigTool._build_config_template(datasource_id, skill_id)
await self.client.post(
"/datasource/skill/updateOrGet",
json_data={"datasourceId": datasource_id, "configTemplate": config_template},
)
# 6. confirmTools 建工具
sql_params = args.get("sqlParams")
if isinstance(sql_params, dict):
sql_params = json.dumps(sql_params)
elif not sql_params:
sql_params = '{"type":"object","required":[],"properties":{}}'
suggestion = {
"name": args["name"],
"businessDescription": args["businessDescription"],
"sqlTemplate": sql_template,
"sqlParams": sql_params,
"resultType": args.get("resultType", "list"),
"businessScenario": args.get("businessScenario", "数据查询场景"),
}
# tableIds前端真机始终传 ""空串。None / 空列表都归一为 "",与前端一致;
# 仅当调用方显式给了非空列表时才透传该列表。
table_ids = args.get("tableIds")
confirm_body = {
"skillId": skill_id,
"tableIds": table_ids if table_ids else "",
"suggestions": [suggestion],
}
try:
confirm_result = await self.client.post(
"/datasource/skill/confirmTools", json_data=confirm_body
)
except Exception as e:
# 工具创建失败:若本次刚建了技能,此刻技能是「空技能」(有配置无工具)——
# 正是要避免的无效状态。明确告知调用方:技能已建好,重跑本工具即可补上工具
# (重跑会走 skillBool=true 分支,仅补工具,幂等安全)。
if created_skill:
raise Exception(
f"技能已创建skillId={skill_id})但工具创建失败:{e}"
"当前技能为「空技能」,请用相同参数重新调用本工具补上工具"
"(重跑只会补工具、不会重复建技能)。"
) from e
raise
return {
"success": True,
"skillId": skill_id,
"skillCreated": created_skill,
"result": confirm_result,
}

View File

@@ -0,0 +1,45 @@
"""
SQL 执行工具 (工具 33)
"""
from ._base import register_tool, ToolDef
@register_tool("execute_sql")
class ExecuteSqlTool(ToolDef):
name = "execute_sql"
description = "执行原生 SQL 查询"
input_schema = {
"type": "object",
"properties": {
"datasourceId": {"type": "string", "description": "数据源 ID"},
"sql": {"type": "string", "description": "SQL 语句"},
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
"sqlTemplate": {"type": "string", "description": "SQL 模板(可选)"},
"businessName": {"type": "string", "description": "业务名称(可选)"},
"params": {"type": "object", "description": "SQL 参数对象(可选)"},
},
"required": ["datasourceId", "sql"],
}
async def execute(self, args: dict) -> dict:
# 映射参数名为后端 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"]
# target 通过 query 参数下发prod/test 双环境),默认 prod
target = args.get("target", "prod")
params = {"target": target} if target else None
return await self.client.post(
"/datasource/sqlExecutionLog/testSqlWithSchema",
json_data=body,
params=params,
)

View File

@@ -0,0 +1,31 @@
"""
表订阅工具 (工具 32)
"""
from ._base import register_tool, ToolDef
@register_tool("toggle_table_subscription")
class ToggleTableSubscriptionTool(ToolDef):
name = "toggle_table_subscription"
description = "切换表的订阅状态(订阅依赖该表已配置 MQTT 字段关联,否则后端会报操作失败)"
input_schema = {
"type": "object",
"properties": {
"configId": {"type": "string", "description": "库/配置 ID即 list_databases 返回的 config id"},
"tableName": {"type": "string", "description": "表名(注意:后端按表名而非 tableId 识别)"},
"subscribe": {"type": "boolean", "description": "true=订阅, false=取消订阅"},
},
"required": ["configId", "tableName", "subscribe"],
}
async def execute(self, args: dict) -> dict:
# 后端真实字段已真机探测确认configId + tableName + isSubscribe(bool)
# 兼容旧参数名 datasourceId->configId
config_id = args.get("configId") or args.get("datasourceId")
body = {
"configId": config_id,
"tableName": args.get("tableName"),
"isSubscribe": args.get("subscribe"),
}
return await self.client.post("/datasource/subscription/toggle", json_data=body)

View File

@@ -0,0 +1,139 @@
"""
表数据 CRUD 工具 (工具 13-17)
"""
import base64
from ._base import register_tool, ToolDef
@register_tool("query_table_data")
class QueryTableDataTool(ToolDef):
name = "query_table_data"
description = "查询内置表数据分页。target 默认 test调试环境查询线上数据须显式传 target=prod"
input_schema = {
"type": "object",
"properties": {
"tableId": {"type": "string", "description": "表 ID"},
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test调试线上数据传 prod"},
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
"pageSize": {"type": "integer", "default": 10, "description": "每页数量"},
},
"required": ["tableId"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
table_id = args.pop("tableId")
args.setdefault("target", "test")
params = {k: v for k, v in args.items() if v is not None}
return await self.client.get(f"/datasource/connection/builtin/table/{table_id}", params=params)
@register_tool("insert_table_row")
class InsertTableRowTool(ToolDef):
name = "insert_table_row"
description = "向内置表插入一行数据。⚠️ 写操作 target 默认 test调试环境写入线上库须显式传 target=prod"
input_schema = {
"type": "object",
"properties": {
"tableId": {"type": "string", "description": "表 ID"},
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test调试写入线上库必须显式传 prod"},
"data": {"type": "object", "description": "行数据(键值对,键为字段名)"},
},
"required": ["tableId", "data"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
table_id = args.pop("tableId")
target = args.pop("target", "test")
data = args.pop("data", {})
params = {"target": target} if target else {}
body = {"data": data}
return await self.client.post(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=body, params=params)
@register_tool("update_table_row")
class UpdateTableRowTool(ToolDef):
name = "update_table_row"
description = "更新内置表的指定行。⚠️ 写操作 target 默认 test调试环境修改线上库须显式传 target=prod"
input_schema = {
"type": "object",
"properties": {
"tableId": {"type": "string", "description": "表 ID"},
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test调试修改线上库必须显式传 prod"},
"primaryKey": {"type": "object", "description": "主键值(如 {\"id\": 1}"},
"data": {"type": "object", "description": "要更新的字段值"},
},
"required": ["tableId", "primaryKey", "data"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
table_id = args.pop("tableId")
target = args.pop("target", "test")
primary_key = args.pop("primaryKey")
data = args.pop("data", {})
params = {"target": target} if target else {}
body = {"primaryKey": primary_key, "data": data}
return await self.client.put(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=body, params=params)
@register_tool("delete_table_rows")
class DeleteTableRowsTool(ToolDef):
name = "delete_table_rows"
description = "删除内置表的指定行(根据主键批量删除)。⚠️ 写操作 target 默认 test调试环境删除线上库数据须显式传 target=prod"
input_schema = {
"type": "object",
"properties": {
"tableId": {"type": "string", "description": "表 ID"},
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test调试删除线上库数据必须显式传 prod"},
"primaryKeys": {
"type": "array",
"description": "主键数组(如 [{\"id\": 1}, {\"id\": 2}]",
"items": {"type": "object"},
},
},
"required": ["tableId", "primaryKeys"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
table_id = args.pop("tableId")
target = args.pop("target", "test")
primary_keys = args.pop("primaryKeys")
params = {"target": target} if target else {}
body = {"primaryKeys": primary_keys}
return await self.client.delete(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=body, params=params)
@register_tool("export_table_excel")
class ExportTableExcelTool(ToolDef):
name = "export_table_excel"
description = "导出表数据为 Excel 文件(返回 base64 编码。target 默认 test调试环境导出线上数据须显式传 target=prod"
input_schema = {
"type": "object",
"properties": {
"tableId": {"type": "string", "description": "表 ID"},
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test调试线上数据传 prod"},
},
"required": ["tableId"],
}
async def execute(self, args: dict) -> dict:
args = dict(args)
table_id = args.pop("tableId")
target = args.pop("target", "test")
params = {"target": target} if target else {}
result = await self.client.get(f"/datasource/connection/builtin/table/{table_id}/export/excel", params=params)
# 处理二进制响应
if result.get("raw"):
content = result["data"]
return {
"success": True,
"file_base64": base64.b64encode(content).decode("utf-8"),
"message": "Excel 文件已导出,请解码 base64 内容获取文件",
}
return result

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
from .env_config import (
get_api_key,
get_base_url,
get_env_config,
get_account,
get_password,
)
from .logger_config import setup_system_logging, get_logger
from .api_client import AgileDBAPIClient, get_default_client
__all__ = [
'get_api_key',
'get_base_url',
'get_env_config',
'get_account',
'get_password',
'setup_system_logging',
'get_logger',
'AgileDBAPIClient',
'get_default_client',
]

View File

@@ -0,0 +1,381 @@
"""
数据库管理平台 API 调用客户端
用于调用数据库管理平台的所有 API 接口
"""
import asyncio
import httpx
import json
from typing import Dict, Any, Optional
from .env_config import (
get_api_key,
get_base_url,
get_account,
get_password,
)
from .logger_config import get_logger
logger = get_logger(__name__)
# 默认超时配置(秒)
DEFAULT_TIMEOUT = 30.0
# 登录接口路径base_url 已含 /api 前缀,此处不重复带)
LOGIN_PATH = "/login"
# 登录类型(平台固定为 user
LOGIN_TYPE = "user"
class AgileDBAPIClient:
"""数据库管理平台 API 客户端
认证支持两种方式(优先级从高到低):
1. 显式 api_key / 环境变量 API_KEY —— 直接作为 Bearer token 使用;
2. 账号密码(环境变量 AGILE_DB_ACCOUNT / AGILE_DB_PASSWORD—— 懒登录,
首次请求时自动调用 /login 换取 token 并缓存token 失效401
自动重新登录并重试一次。
"""
def __init__(
self,
base_url: Optional[str] = None,
api_key: Optional[str] = None,
account: Optional[str] = None,
password: Optional[str] = None,
default_timeout: float = DEFAULT_TIMEOUT,
):
"""
初始化 API 客户端
Args:
base_url: API 基础 URL默认从环境变量 backendBaseUrl 读取)
api_key: API 密钥(默认从环境变量 API_KEY 读取,可为空)
account: 登录账号(默认从环境变量 AGILE_DB_ACCOUNT 读取)
password: 登录密码(默认从环境变量 AGILE_DB_PASSWORD 读取)
default_timeout: 请求超时时间(秒),默认 30 秒
"""
self.base_url = (base_url if base_url is not None else get_base_url()).rstrip('/')
# 显式配置的 api_key 直接作为 token 使用(去掉可能存在的 Bearer 前缀,统一在 _get_headers 拼)
explicit_key = api_key if api_key is not None else get_api_key()
self._token: Optional[str] = self._strip_bearer(explicit_key) or None
self.account = account if account is not None else get_account()
self.password = password if password is not None else get_password()
self.default_timeout = default_timeout
self._client: Optional[httpx.AsyncClient] = None
# 串行化登录,避免并发请求同时触发多次登录
self._login_lock = asyncio.Lock()
logger.info(
f"[客户端初始化] base_url={self.base_url}, "
f"认证方式={'api_key' if self._token else ('account:' + self.account if self.account else '未配置')}"
)
@staticmethod
def _strip_bearer(value: Optional[str]) -> str:
"""去掉 token 字符串可能携带的 'Bearer ' 前缀"""
if not value:
return ""
value = value.strip()
return value[7:].strip() if value.lower().startswith("bearer ") else value
@property
def client(self) -> httpx.AsyncClient:
"""懒加载 HTTP 客户端"""
if self._client is None:
self._client = httpx.AsyncClient(timeout=self.default_timeout)
return self._client
async def _ensure_token(self) -> str:
"""确保已有可用 token没有则登录获取"""
if self._token:
return self._token
return await self._login()
async def _login(self) -> str:
"""换取 token 并缓存(并发安全,用于首次登录)"""
async with self._login_lock:
# 双重检查:可能在等锁期间已有其它协程完成登录
if self._token:
return self._token
return await self._do_login()
async def _relogin(self, stale_token: Optional[str]) -> str:
"""登录态失效后重新登录compare-and-swap并发安全
仅当当前 token 仍是那次失败请求所用的旧 token 时才真正重登;
若在等锁期间已有其它协程刷新过 token则直接复用新 token
避免把别人刚拿到的新 token 抹掉又触发一次多余的重登。
"""
async with self._login_lock:
if self._token != stale_token:
# 别的协程已经刷新过 token直接用新的
return self._token or ""
self._token = None
return await self._do_login()
async def _do_login(self) -> str:
"""实际执行 /login 的逻辑(调用方需自行持有 _login_lock"""
if not self.account or not self.password:
raise Exception(
"未配置认证信息:请设置环境变量 API_KEY"
"或同时设置 AGILE_DB_ACCOUNT 和 AGILE_DB_PASSWORD"
)
url = self._build_url(LOGIN_PATH)
payload = {
"username": self.account,
"password": self.password,
"loginType": LOGIN_TYPE,
}
logger.info(f"[登录] POST {url}, username={self.account}, loginType={LOGIN_TYPE}")
try:
response = await self.client.post(
url,
headers={"Content-Type": "application/json"},
json=payload,
)
except httpx.TimeoutException:
raise Exception(f"登录请求超时: {url}")
except httpx.RequestError as e:
raise Exception(f"登录请求异常: {url}, 错误: {str(e)}")
is_json, body = self._try_parse_json(response)
data = self._handle_response(response, url, is_json, body)
# 平台登录响应token 在顶层 token 字段
token = data.get("token") if isinstance(data, dict) else None
if not token:
raise Exception(f"登录成功但未返回 token响应: {json.dumps(data, ensure_ascii=False)[:300]}")
self._token = self._strip_bearer(token)
logger.info("[登录] 成功获取 token")
return self._token
@staticmethod
def _try_parse_json(response: httpx.Response):
"""尝试把响应体解析为 JSON只解析一次供后续复用。
Returns:
(is_json, data):是 JSON 则 (True, 解析结果)
二进制/非 JSON 响应(如 Excel 下载)则 (False, None)。
"""
try:
return True, response.json()
except (json.JSONDecodeError, UnicodeDecodeError):
return False, None
@staticmethod
def _is_unauthorized(response: httpx.Response, is_json: bool, data: Any) -> bool:
"""判断响应是否为登录态失效(基于已解析好的 body不重复 parse
平台有两种表达 401 的方式,都需识别:
1. HTTP 状态码 401
2. HTTP 200 但 body 信封里 code=401{"code":401,"msg":"登录过期,请重新登录"})。
"""
if response.status_code == 401:
return True
return is_json and isinstance(data, dict) and data.get("code") == 401
@staticmethod
def _rewind_files(files: Optional[Dict[str, Any]]) -> None:
"""把上传用的文件流游标重置到开头。
401 重试会复用同一个 files 二次发送,而文件流在第一次发送后游标已到末尾,
不 rewind 会导致重试上传空内容。支持两种形态:
- 直接的文件对象;
- (filename, fileobj, content_type) 元组httpx multipart 常用写法)。
"""
if not files:
return
for value in files.values():
fileobj = value
if isinstance(value, (tuple, list)) and len(value) >= 2:
fileobj = value[1]
seek = getattr(fileobj, "seek", None)
if callable(seek):
try:
seek(0)
except (OSError, ValueError):
# 不可重置的流(如已关闭/不支持 seek静默跳过交由上传结果反映
pass
def _get_headers(self, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
"""获取请求头"""
headers = {}
if self._token:
headers['Authorization'] = f'Bearer {self._token}'
if extra_headers:
headers.update(extra_headers)
return headers
def _build_url(self, path: str) -> str:
"""构建完整 URL
约定base_url 已包含完整地址(如含 /api 前缀则配在环境变量里),
各工具传入的 path 不带 /api 前缀,此处直接拼接。
"""
if path.startswith('http://') or path.startswith('https://'):
return path
return f"{self.base_url}{path}"
def _handle_response(
self,
response: httpx.Response,
url: str,
is_json: Optional[bool] = None,
data: Any = None,
) -> Dict[str, Any]:
"""统一处理 API 响应
is_json / data 为调用方已解析好的 body避免对大响应重复 parse
未传入时此处自行解析一次。
"""
logger.info(f"[API响应] HTTP {response.status_code}")
if response.status_code == 204:
return {"success": True, "data": None}
# 复用调用方解析结果;未提供则在此解析一次
if is_json is None:
is_json, data = self._try_parse_json(response)
# 先看 body 再判断状态码。
# 平台会用非 2xx 状态码 + body 里的 {code, msg} 返回业务错误,
# 若先 raise_for_status() 会在拿到 body 前抛异常,导致真正的 msg 全部丢失。
if not is_json:
# 非 JSON 响应(如 Excel/文件下载,二进制内容 .json() 会抛 UnicodeDecodeError
response.raise_for_status()
return {"success": True, "data": response.content, "raw": True}
# 检查平台 API 的 {code, msg} 格式
# 平台约定code 为 200 或 0 均表示成功(见数据库模块文档 §1.1
if isinstance(data, dict) and 'code' in data:
if data['code'] not in (0, 200):
error_msg = data.get('msg', '未知错误')
logger.error(f"[API错误] HTTP {response.status_code}, {error_msg}")
raise Exception(error_msg)
# code 合法即视为业务成功,不再用 HTTP 状态码二次否决
return data
# 无 {code} 信封的响应:回落到 HTTP 状态码判断,
# 非 2xx 时把 body 文本带进异常,避免错误细节丢失。
if response.status_code >= 400:
detail = data if isinstance(data, str) else json.dumps(data, ensure_ascii=False)
logger.error(f"[API错误] HTTP {response.status_code}, {detail}")
raise Exception(f"HTTP {response.status_code}: {detail}")
return data
async def _request(
self,
method: str,
path: str,
*,
params: Optional[Dict[str, Any]] = None,
json_data: Optional[Dict[str, Any]] = None,
files: Optional[Dict[str, Any]] = None,
extra_headers: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""统一请求入口自动注入认证、登录态失效401时重登重试一次"""
url = self._build_url(path)
# 账号密码模式下首次请求前先确保有 token纯 api_key 模式 _ensure_token 直接返回
await self._ensure_token()
async def _send() -> httpx.Response:
headers = self._get_headers(extra_headers)
return await self.client.request(
method, url, headers=headers, params=params, json=json_data, files=files
)
try:
logger.info(f"[API请求] {method} {url}")
# 记下本次请求所用 token供 401 时做 compare-and-swap 重登
token_used = self._token
response = await _send()
is_json, body = self._try_parse_json(response)
# token 失效:仅在账号密码模式下尝试重新登录并重试一次。
# 平台可能用 HTTP 401也可能用 HTTP 200 + body code=401 表达登录过期,
# 两者都要识别(见 _is_unauthorized
if self._is_unauthorized(response, is_json, body) and self.account and self.password:
logger.warning("[认证] 收到 401登录过期尝试重新登录后重试一次")
# CAS 重登:仅当 token 仍是本次用的旧值才真正重登,否则复用别人刚拿到的新 token
await self._relogin(token_used)
# 重试前把上传文件流游标重置到开头,避免二次发送空内容
self._rewind_files(files)
response = await _send()
is_json, body = self._try_parse_json(response)
# 重登后仍判定为登录态失效:账号密码本身失效/被禁,给明确报错
if self._is_unauthorized(response, is_json, body):
logger.error("[认证] 重新登录后仍返回 401")
raise Exception("重新登录后仍未通过认证,请检查账号密码是否正确或账号是否被禁用")
return self._handle_response(response, url, is_json, body)
except httpx.TimeoutException:
raise Exception(f"API 请求超时: {url}")
except httpx.HTTPStatusError as e:
raise Exception(f"API 请求失败 (HTTP {e.response.status_code}): {url}")
except httpx.RequestError as e:
raise Exception(f"API 请求异常: {url}, 错误: {str(e)}")
async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""发送 GET 请求"""
return await self._request("GET", path, params=params)
async def post(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""发送 POST 请求"""
return await self._request(
"POST", path, params=params, json_data=json_data,
extra_headers={'Content-Type': 'application/json'},
)
async def put(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""发送 PUT 请求"""
return await self._request(
"PUT", path, params=params, json_data=json_data,
extra_headers={'Content-Type': 'application/json'},
)
async def delete(self, path: str, params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""发送 DELETE 请求
平台部分 DELETE 接口需带 body统一走通用 request() 处理。
"""
extra = {'Content-Type': 'application/json'} if json_data is not None else None
return await self._request("DELETE", path, params=params, json_data=json_data, extra_headers=extra)
async def upload(self, path: str, files: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""发送文件上传请求multipart/form-data
不显式设置 Content-Typehttpx 会根据 files 自动生成 multipart 边界。
"""
return await self._request("POST", path, params=params, files=files)
async def close(self):
"""关闭 HTTP 客户端"""
if self._client is not None:
await self._client.aclose()
self._client = None
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()
return False
# 懒加载的默认客户端
_default_client: Optional[AgileDBAPIClient] = None
def get_default_client() -> AgileDBAPIClient:
"""获取默认客户端(懒加载)"""
global _default_client
if _default_client is None:
_default_client = AgileDBAPIClient()
return _default_client

View File

@@ -0,0 +1,68 @@
"""环境变量配置模块 - 数据库管理平台 MCP Server"""
import os
from typing import Optional
def get_api_key(default: Optional[str] = None) -> str:
"""
获取 API 密钥(可选)
优先级:显式配置了 API_KEY 时直接使用;未配置则返回空串,
由客户端回落到账号密码登录流程换取 token。
Args:
default: 默认值(可选)
Returns:
str: API 密钥,未配置时为空串
"""
return os.environ.get("API_KEY", default or "")
def get_account(default: Optional[str] = None) -> str:
"""获取登录账号(环境变量 AGILE_DB_ACCOUNT"""
return os.environ.get("AGILE_DB_ACCOUNT", default or "")
def get_password(default: Optional[str] = None) -> str:
"""获取登录密码(环境变量 AGILE_DB_PASSWORD"""
return os.environ.get("AGILE_DB_PASSWORD", default or "")
def get_base_url(default: str = "http://lzwcai-demp-corp-manager:8086") -> str:
"""
获取后端服务地址
Args:
default: 默认值(默认 http://lzwcai-demp-corp-manager:8086
Returns:
str: 后端 API 基础 URL
"""
return os.environ.get("backendBaseUrl", default)
def get_env_config() -> dict:
"""
获取所有环境配置
Returns:
dict: 包含所有配置的字典
"""
return {
"api_key": os.environ.get("API_KEY", ""),
"account": os.environ.get("AGILE_DB_ACCOUNT", ""),
"base_url": get_base_url(),
}
def set_env_variable(key: str, value: str) -> None:
"""
设置环境变量(仅在当前进程中有效)
Args:
key: 环境变量名
value: 环境变量值
"""
os.environ[key] = value

View File

@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
"""
统一日志配置模块
提供系统级别的日志配置和管理
"""
import os
import sys
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
class LoggerConfig:
"""日志配置管理类"""
def __init__(self, logs_dir: str = None):
"""初始化日志配置
Args:
logs_dir: 日志目录路径默认为项目根目录下的logs文件夹
"""
if logs_dir:
self.logs_dir = Path(logs_dir)
else:
project_root = Path(__file__).parent.parent
self.logs_dir = project_root / "logs"
self.logs_dir.mkdir(exist_ok=True)
self.log_format = '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
self.date_format = '%Y-%m-%d %H:%M:%S'
self.log_level = self._get_log_level_from_env()
self._initialized = False
def _get_log_level_from_env(self) -> int:
log_level_str = os.getenv('LOG_LEVEL', 'INFO').upper()
level_mapping = {
'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
'WARNING': logging.WARNING,
'WARN': logging.WARNING,
'ERROR': logging.ERROR,
'CRITICAL': logging.CRITICAL,
'FATAL': logging.CRITICAL
}
return level_mapping.get(log_level_str, logging.INFO)
def setup_logging(self,
app_name: str = "lzwcai_mcp_agile_db",
log_level: int = logging.INFO,
max_file_size: int = 10 * 1024 * 1024,
backup_count: int = 5,
console_output: bool = True) -> logging.Logger:
if self._initialized:
return logging.getLogger()
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
formatter = logging.Formatter(self.log_format, self.date_format)
# 1. 主日志文件 - 按大小滚动
main_log_file = self.logs_dir / f"{app_name}.log"
file_handler = RotatingFileHandler(
main_log_file,
maxBytes=max_file_size,
backupCount=backup_count,
encoding='utf-8'
)
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# 2. 错误日志文件
error_log_file = self.logs_dir / f"{app_name}_error.log"
error_handler = RotatingFileHandler(
error_log_file,
maxBytes=max_file_size,
backupCount=backup_count,
encoding='utf-8'
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(formatter)
root_logger.addHandler(error_handler)
# 3. 控制台输出 (MCP协议使用stdio时必须将日志输出到stderr)
if console_output:
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(log_level)
console_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
self.date_format
)
console_handler.setFormatter(console_formatter)
root_logger.addHandler(console_handler)
self._initialized = True
root_logger.info(f"日志系统初始化完成 - 日志目录: {self.logs_dir}")
return root_logger
def get_module_logger(self, name: str) -> logging.Logger:
"""获取模块级别的 logger继承根配置"""
return logging.getLogger(name)
# 全局日志配置实例
logger_config = LoggerConfig()
def setup_system_logging(app_name: str = "lzwcai_mcp_agile_db",
log_level: int = logging.INFO) -> logging.Logger:
return logger_config.setup_logging(app_name, log_level)
def get_logger(name: str) -> logging.Logger:
return logger_config.get_module_logger(name)

View File

@@ -0,0 +1,454 @@
# 数据库模块 —— 功能、接口与业务链路说明
> 适用范围:`src/components/databasePage/` 全部组件 + `src/server/database.ts` API 层
> 数据源分两类:**外置数据源external** 与 **内置数据源builtin / 内置 PostgreSQL**
> 所有接口前缀均为 `/api/datasource`MQTT 同步相关为 `/api/system/mqtt`
> 大部分接口超时设置为 `300000ms`5 分钟,因含 AI 处理)
---
## 0. 名词与基础概念
| 名词 | 说明 |
| --- | --- |
| 连接 / 数据源connection | 一条数据库连接记录,`sourceType``external``builtin` |
| 配置config | 数据源下「数据库 + 选中表」的关联配置,一条 config 对应一个库 |
| 表元数据table metadata | 平台侧记录的表结构字段、注释、AI 训练描述等) |
| 技能skill | 绑定到某数据源的「智能问数技能」 |
| 工具tool | 技能下的一条可复用 SQL 查询能力(沉淀为 MCP 工具) |
| 环境target | 内置数据源数据分 `prod`(线上)/ `test`(调试)两套 |
| status | 数据源状态:`0` 运行中,`1` 已停止 |
### 两类数据源核心差异
| 维度 | 外置数据源 external | 内置数据源 builtin |
| --- | --- | --- |
| 来源 | 连接用户已有的远程数据库 | 平台内置 PostgreSQL平台直接建库建表 |
| 默认数字员工 | `1001`SQL处理助手 | `1002`SQL处理助手 |
| 建表能力 | ❌ 只读连接,不能建表 | ✅ AI 智能建表 / Excel 导入建表 / 手动建表 |
| 数据增删改 | ❌ 不提供行级 CRUD | ✅ 行级增删改查 + Excel 导入导出 |
| 环境切换 | ❌ 无 | ✅ prod / test 双环境 |
| 表订阅同步 | ❌ | ✅ MQTT 数据变更订阅 |
| 创建入口 | `CreateDataSource.vue` | `CreateBuiltinDataSource.vue` |
---
## 1. 数据源(连接)的增删改查 —— 内外置通用
入口:[DataSourcePageMain.vue](../src/components/databasePage/DataSourcePageMain.vue) → [DataSourceList.vue](../src/components/databasePage/DataSourceList.vue)
### 1.1 查询(列表 / 详情)
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- | --- |
| 数据源列表(分页/搜索/状态筛选) | `fetchDataSources()` | `getConnectionList(params)` | GET | `/api/datasource/connection/list` |
| 数据源详情 | `fetchDataSourceDetail()` | `getConnectionDetail(id)` | GET | `/api/datasource/connection/{id}` |
| 连接实例详情(含库表结构) | — | `getConnectionInstanceDetail(id)` | GET | `/api/datasource/connection/{id}` |
| 查看数据源实时库表结构 | `loadDatabaseList()` | `getConnectionRealtimeStructure(id)` | GET | `/api/datasource/connection/realtime/structure/{id}` |
- 列表查询参数:`{ datasourceName?, status?, pageNum, pageSize }`,前端用手动 `IntersectionObserver` 做无限滚动。
- 响应判定统一:`code === 200 || code === 0` 视为成功,列表数据在 `response.rows`,总数在 `response.total`
### 1.2 启用 / 停用
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- | --- |
| 切换运行状态 | `handleChangeStatus()` | `putConnectionChangeStatus({ id, status })` | PUT | `/api/datasource/connection/changeStatus` |
- `status` 切换:`1->0` 启用,`0->1` 停用。
### 1.3 删除
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- | --- |
| 删除数据源 | `handleDelete()` | `deleteConnection(id)` | DELETE | `/api/datasource/connection/{id}` |
**业务链路(删除)**
```
用户点删除 → Modal 二次确认
→ 若 status===0运行中先 putConnectionChangeStatus({id, status:1}) 停用
→ 等待 500ms 确保状态更新
→ deleteConnection(id)
→ 成功后 fetchDataSources() 刷新列表
```
---
## 2. 外置数据源external的创建与编辑
入口组件:[CreateDataSource.vue](../src/components/databasePage/CreateDataSource.vue)4 步向导)
### 2.1 步骤与接口
```
步骤0 选择数据库类型mysql/postgresql/oracle/sqlserver/dameng/kingbase/sqlite/mariadb
步骤1 配置连接信息host/port/库名/用户名/密码/认证方式)
→ 点「下一步」自动触发 testConnection() 测试连接
→ 测试通过 → postConnectionDetail() / putConnectionDetail() 创建/更新数据源
→ emit('refresh') 通知列表刷新
→ getConnectionRealtimeStructure() 拉取真实库表结构
步骤2 选择数据库和数据表大数据量shallowRef + 分批渲染 + 防抖搜索)
步骤3 确认信息
→ postConnectionConfig() / putConnectionConfig() 落库「库表关联」配置
→ emit('success')
```
### 2.2 接口清单
| 功能 | 接口函数 | 方法 | 路径 | 关键入参 |
| --- | --- | --- | --- | --- |
| 测试连接 | `testConnection(data)` | POST | `/api/datasource/connection/test` | host/port/databaseName/datasourceType/username/password/connectionType |
| 创建数据源 | `postConnectionDetail(data)` | POST | `/api/datasource/connection` | 同上 + datasourceName/remark |
| 更新数据源 | `putConnectionDetail(data)` | PUT | `/api/datasource/connection` | 同上 + id编辑时密码可空表示不改 |
| 拉取实时库表结构 | `getConnectionRealtimeStructure(id)` | GET | `/api/datasource/connection/realtime/structure/{id}` | 数据源 id |
| 创建库表关联配置 | `postConnectionConfig(data)` | POST | `/api/datasource/config` | connectionId / syncTables / databases[] |
| 更新库表关联配置 | `putConnectionConfig(data)` | PUT | `/api/datasource/config` | id + 同上 |
| 删除配置 | `deleteConnectionConfig(ids[])` | POST | `/api/datasource/config/deletes` | `{ ids: [] }` |
- `postConnectionConfig` 请求体结构:
```json
{
"id": "配置ID编辑传",
"connectionId": "数据源ID",
"syncTables": true,
"databases": [
{ "databaseName": "库名", "tableNames": ["表1", "表2"] }
]
}
```
- 编辑模式不回显密码;密码留空 = 不修改密码。
---
## 3. 内置数据源builtin / 内置 PostgreSQL的创建与编辑
入口组件:[CreateBuiltinDataSource.vue](../src/components/databasePage/CreateBuiltinDataSource.vue)
支持三种模式:`create`(新建数据源)/ `edit`(编辑数据源)/ `addTable`(为已有库新增表)。
### 3.1 步骤与接口create 模式)
```
步骤0 填写数据源信息datasourceName / remark
→ postCreateBuiltinPostgreSQLConnection() 创建内置 PG 连接,返回 connectionId
步骤1 填写业务场景描述 + 数据库名(仅字母数字下划线,自动加 _数据源id 后缀)
→ AI 生成表结构postGenerateTable() 返回 taskId
→ 轮询 getAiTrainingDetail(taskId)trainingStatus: 0待训/1训练中/2完成/3失败最多 60 次 × 2s
→ 解析 createTableData.data.tables 填入表列表
→ postCreateDatabase(connectionId, { databaseName }) 创建数据库
步骤2 预览 / 编辑表结构TableDetailEditor
→ handleSubmit() 遍历表逐张 postCreateTable(connectionId, requestData)
→ emit('refresh') + emit('success')
```
### 3.2 接口清单
| 功能 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- |
| 创建内置 PG 连接 | `postCreateBuiltinPostgreSQLConnection(data)` | POST | `/api/datasource/connection/create_builtin_postgresql` |
| 修改内置 PG 连接 | `putUpdateBuiltinDatabase(data)` | PUT | `/api/datasource/connection/update_builtin_database` |
| AI 生成表结构 | `postGenerateTable(data)` | POST | `/api/datasource/connection/generate_table` |
| 查询 AI 训练任务详情(轮询) | `getAiTrainingDetail(taskId)` | GET | `/api/ai/training/{taskId}` |
| 创建数据库 | `postCreateDatabase(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_database` |
| 创建数据表 | `postCreateTable(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_table` |
| 创建库+表(合并) | `postCreateDatabaseTable(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_database_table` |
- `addTable` 模式不直接调建表接口,而是 `emit('submitTables', tables)` 交回父组件(如 DatabaseDetail提交。
- AI 业务描述润色走 `getAgentGeneric` + `AGENT_GENERIC_CODES.SQL_SCENARIO_DESCRIPTION``server/employee`)。
---
## 4. 数据库 / 表结构的增删改查(主要面向内置源)
入口组件:[DatabaseDetail.vue](../src/components/databasePage/DatabaseDetail.vue)(左侧表列表 + 右侧四视图)
### 4.1 表 / 字段的查询
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- | --- |
| 表元数据列表(分页/无限滚动) | `getTablesList()` | `getTableAiList(params)` | GET | `/api/datasource/table/ailist` |
| 表详情(字段列表) | `executeSelectTable()` | `getTableDetail(id)` | GET | `/api/datasource/table/{id}/detail` |
| 表列表(基础) | — | `getTableList(data)` | GET | `/api/datasource/table/list` |
- `getTableAiList` 参数:`{ datasourceId, pageNum, pageSize }`(这里 `datasourceId` 实为「配置/库」id
- 切表有 Race Condition 防护:对比 `currentTableId` 拦截过期的详情请求。
### 4.2 库 / 表的增删改
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- | --- |
| 修改数据库(改名) | `handleDatabaseSettingsConfirm()` | `putAlterDatabase(connectionId, data)` | PUT | `/api/datasource/connection/{connectionId}/alter_database` |
| 删除库(配置) | `handleDeleteDatabase()` | `deleteConnectionConfig(ids[])` | POST | `/api/datasource/config/deletes` |
| AI 智能建表(入口) | `createNewTable()` | 仅打开弹窗(内嵌 `CreateBuiltinDataSource` 的 addTable 模式) | — | — |
| 批量建表(提交) | `handleSubmitTables(tables)` | 循环 `postCreateTable(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_table` |
| 修改表结构 | `handleSaveTable()` | `putAlterTable(connectionId, data)` | PUT | `/api/datasource/connection/{connectionId}/alter_table` |
- 修改库后会重新 `getConnectionDetail(id)` 并 `emit('refresh', updatedDataSource)` 同步父级。
- 改表用增量 operation 标记字段:`ADD_COLUMN` / `MODIFY_COLUMN` / `DROP_COLUMN`(来自 TableDetailEditor
### 4.3 智能导入建表Excel
入口组件:[TableRecognition.vue](../src/components/databasePage/TableRecognition.vue)(两步向导)
```
步骤0 上传 Excel≤500KB+ 选环境
→ postImportDocumentPreview(connectionId, file, target) AI 识别前10条 → 表结构预览
步骤1 编辑确认表结构与数据
→ postImportDocumentConfirm(connectionId, data, {target}) 建表 + 插入数据
→ emit('complete')
```
| 功能 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- |
| 文档导入预览AI识别 | `postImportDocumentPreview(connectionId, file, target)` | POST | `/api/datasource/connection/{connectionId}/import_document/preview` |
| 文档导入确认(建表+插数据) | `postImportDocumentConfirm(connectionId, data, params)` | POST | `/api/datasource/connection/{connectionId}/import_document/confirm` |
---
## 5. 内置表「数据行」的增删改查builtin 专属prod/test 双环境)
入口组件:[CustomizeDbTable.vue](../src/components/databasePage/CustomizeDbTable.vue)(包裹通用 `CommonDbTable`
所有接口都带 `target` 参数区分线上 / 调试环境。
| 功能 | 组件回调 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- | --- |
| 查(分页拉数据) | `onFetch` | `getBuiltinTableData(tableId, {pageNum,pageSize,target})` | GET | `/api/datasource/connection/builtin/table/{tableId}` |
| 增(新增行) | `onAdd` | `postBuiltinTableRows(tableId, data, {target})` | POST | `/api/datasource/connection/builtin/table/{tableId}/rows` |
| 改(更新行) | `onEdit` | `putBuiltinTableRows(tableId, data, {target})` | PUT | `/api/datasource/connection/builtin/table/{tableId}/rows` |
| 删(删除行) | `onDelete` | `deleteBuiltinTableRows(tableId, {primaryKeys}, {target})` | DELETE | `/api/datasource/connection/builtin/table/{tableId}/rows` |
| 导出 Excel | `onExport` | `getBuiltinTableExportExcel(tableId, {target})` | GET | `/api/datasource/connection/builtin/table/{tableId}/export/excel` |
| 导入数据 | `onImport` → `handleImportComplete` | `postImportDocumentConfirm(...)` | POST | 见上 |
- `onFetch` 把后端二维数组 `content` 按列名映射成对象数组。
- `onEdit/onDelete` 用动态主键(`rowKey`,默认 `id`)拼装 `primaryKey` / `primaryKeys`。
- 删除入参结构:`{ primaryKeys: [ { 主键字段: 值 }, ... ] }`。
- 导出接口为 `responseType: 'blob'`含完整错误兜底blob 是 JSON 时解析出 msg
---
## 6. 智能问数技能skill与工具tool的增删改查
入口组件:[ChatDebugging.vue](../src/components/databasePage/ChatDebugging.vue) + [SqlControllerMsg.vue](../src/components/databasePage/SqlControllerMsg.vue)
### 6.1 接口清单
| 功能 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- |
| 查询数据源配置(含 skillBool | `getConnectionConfig(id)` | GET | `/api/datasource/config/{id}` |
| 按数据源查技能 | `getSkillByDatasource(id)` | GET | `/api/datasource/skill/getByDatasource/{id}` |
| 按技能 ID 查工具列表 | `getSkillBySkillId(id)` | GET | `/api/datasource/skill/getBySkillId/{id}` |
| 创建技能 | `postSkillCreateOrGet(data)` | POST | `/api/datasource/skill/createOrGet` |
| 修改技能 | `putSkillUpdateOrGet(data)` | POST | `/api/datasource/skill/updateOrGet` |
| 确认/创建技能工具 | `postSqlSkillConfirmTools(data)` | POST | `/api/datasource/skill/confirmTools` |
| 修改工具名/描述 | `postSkillToolUpdateOrGet(data)` | POST | `/api/datasource/skill/tskilltool/updateOrGet` |
| 删除技能工具 | `postDeleteSkillTool(skillToolId)` | DELETE | `/api/datasource/skill/tskilltool/{skillToolId}` |
> 说明:`executeSql`POST `/api/datasource/sqlExecutionLog/testSqlWithSchema`)虽在 `server/database.ts` 中定义,但在 databasePage 模块内**未被任何组件调用**——它仅在工作流编辑器 [workflowPage/EditorMain.vue](../src/components/workflowPage/EditorMain.vue) 里使用。智能问数场景下SQL 由后端 AI 通过 SSE 流执行并直接返回结果,前端不再单独调用执行接口。
### 6.2 「把查询沉淀为工具」业务链路SqlControllerMsg 核心)
```
AI 返回 msgType=5 的 SQL 结果 → SqlControllerMsg 解析多视图
用户点「添加到工具」
→ 校验环境test 禁止)、是否选库、重复检测
→ 分两条路径:
┌ skillBool=true技能已存在
│ → postSqlSkillConfirmTools() 直接确认工具
└ skillBool=false技能不存在
→ postSkillCreateOrGet() 创建技能
→ putSkillUpdateOrGet() 写入 MCP 配置模板 lzwcai-mcp-sqlexecutor
→ postSqlSkillConfirmTools() 确认工具
→ eventBus.emit(RELOAD_SKILL_DATA, datasourceId)
→ ChatDebugging 监听到事件 → loadSkillData() 刷新左侧技能/工具列表
```
- 重复检测链路:`getConnectionConfig → getSkillByDatasource → getSkillBySkillId`,比对 `sqlTemplate`。
---
## 7. AI 补全训练
入口:[DatabaseDetail.vue](../src/components/databasePage/DatabaseDetail.vue)「AI补全管理」视图
| 功能 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- |
| 训练任务列表 | `getAiTrainingList(data)` | GET | `/api/ai/training/list` |
| 按选中表创建训练 | `postAiTrainingCreateBySelected(data)` | POST | `/api/ai/training/createBySelected` |
| 训练任务详情 | `getAiTrainingDetail(id)` | GET | `/api/ai/training/{id}` |
- 训练状态:`0` 待训练 / `1` 训练中 / `2` 已完成 / `3` 失败。
---
## 8. 字段关联同步MQTTbuiltin 专属)
入口:[DatabaseDetail.vue](../src/components/databasePage/DatabaseDetail.vue)「字段关联管理」视图
| 功能 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- |
| 同步配置列表 | `getMqttConfigList(params)` | GET | `/api/system/mqtt/config/list` |
| 配置详情 | `getMqttConfigDetail(configId)` | GET | `/api/system/mqtt/config/{configId}` |
| 目标表列表 | `getMqttConfigTableList()` | GET | `/api/system/mqtt/config/tableList` |
| 目标表字段列表 | `getMqttConfigTableColumns(tableName)` | GET | `/api/system/mqtt/config/tableColumns/{tableName}` |
| 新增配置 | `postMqttConfig(data)` | POST | `/api/system/mqtt/config` |
| 修改配置 | `putMqttConfig(data)` | PUT | `/api/system/mqtt/config` |
| 删除配置 | `deleteMqttConfig(ids)` | DELETE | `/api/system/mqtt/config/{ids}` |
| 刷新缓存 | `getMqttConfigRefreshCache()` | GET | `/api/system/mqtt/config/refreshCache` |
| 切换表订阅同步 | `postDatasourceSubscriptionToggle(data)` | POST | `/api/datasource/subscription/toggle` |
> ⚠️ 注意:提交配置时前端故意调换 `sourceTable` / `targetTable`,回显时再换回来(见 DatabaseDetail 的 `applyRelationConfigByTargetTable` 与 `loadRelationConfigByTableId`)。
---
## 9. API 密钥与权限的增删改查
入口组件:[DataSourceKeys.vue](../src/components/databasePage/DataSourceKeys.vue) + [DataSourceKeySetting.vue](../src/components/databasePage/DataSourceKeySetting.vue)
### 9.1 密钥 CRUD
| 功能 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- |
| 密钥列表 | `getApiKeyList(data)` | GET | `/api/datasource/api_key/list` |
| 新增密钥 | `postApiKey(data)` | POST | `/api/datasource/api_key` |
| 修改密钥(含启停) | `putApiKey(data)` | PUT | `/api/datasource/api_key` |
| 删除密钥 | `deleteApiKey(ids)` | DELETE | `/api/datasource/api_key/{ids}` |
### 9.2 权限配置
| 功能 | 接口函数 | 方法 | 路径 |
| --- | --- | --- | --- |
| 查询密钥权限 | `getApiKeyPermission(apiKeyId)` | GET | `/api/datasource/api_key/permission/{apiKeyId}` |
| 批量授予权限 | `postApiKeyPermissionGrantBatch(data)` | POST | `/api/datasource/api_key/permission/grant_batch` |
权限配置走 `DataSourceKeySetting` 四步向导(数据源 → 数据库 → 数据表分三级权限connection / database / table。其中加载列表复用
| 步骤 | 接口函数 | 路径 |
| --- | --- | --- |
| 选数据源 | `getConnectionList` | `/api/datasource/connection/list` |
| 选数据库 | `getConnectionConfigList` | `/api/datasource/config/list` |
| 选数据表 | `getTableList` | `/api/datasource/table/list` |
`postApiKeyPermissionGrantBatch` 请求体batchDatas每项
```json
{
"apiKeyId": "密钥ID",
"batchDatas": [
{
"connectionId": "数据源ID",
"permissionLevel": "connection|database|table",
"permissionType": "read,write逗号拼接",
"databaseName": "库名database/table 级)",
"tableName": "表名table 级)"
}
]
}
```
---
## 10. 端到端业务链路总览
### 链路 A外置数据源「从创建到智能问数」
```
DataSourcePageMain → 添加远程数据源
→ CreateDataSource 4步:
testConnection → postConnectionDetail → getConnectionRealtimeStructure → postConnectionConfig
→ 列表出现卡片 → 点「业务执行」
→ ChatDebugging员工1001三栏 → DataSourcePlugIn 选库 → ChatBusiness 提问
→ 后端 AI 经 SSE 流执行 SQL 并返回结果 (msgType=5) → SqlControllerMsg 多视图渲染
→ 可「添加到工具」沉淀为技能
```
### 链路 B内置数据源「从建库建表到数据管理」
```
DataSourcePageMain → 添加内置数据源
→ CreateBuiltinDataSource:
postCreateBuiltinPostgreSQLConnection
→ postGenerateTable + 轮询 getAiTrainingDetailAI 建表结构)
→ postCreateDatabase → postCreateTable
→ DatabaseDetail 管理:
getTableAiList表列表→ getTableDetail字段
→ CustomizeDbTable 行级 CRUDprod/test
→ 智能导入 TableRecognitionpreview→confirm
→ 字段关联 MQTT / AI 补全训练 / 订阅
→ 业务执行员工1002智能问数同链路 A
```
### 链路 C对外开放API 密钥)
```
DataSourceKeys → postApiKey 建密钥
→ DataSourceKeySetting 配权限(连接→库→表三级)
→ postApiKeyPermissionGrantBatch 批量授权
→ ApiCallDocument 查看调用文档(/API_DOCUMENTATION.md
→ 外部系统凭密钥调用数据查询
```
---
## 附录:完整接口索引(按 server/database.ts 顺序)
| # | 函数 | 方法 | 路径 | 用途 |
| --- | --- | --- | --- | --- |
| 1 | `getTableDetail` | GET | `/api/datasource/table/{id}/detail` | 表详情 |
| 2 | `getConnectionList` | GET | `/api/datasource/connection/list` | 连接列表 |
| 3 | `getConnectionDetail` | GET | `/api/datasource/connection/{id}` | 连接详情 |
| 4 | `getConnectionInstanceDetail` | GET | `/api/datasource/connection/{id}` | 连接实例详情 |
| 5 | `getTableList` | GET | `/api/datasource/table/list` | 表列表 |
| 6 | `getMqttConfigList` | GET | `/api/system/mqtt/config/list` | MQTT配置列表 |
| 7 | `getMqttConfigDetail` | GET | `/api/system/mqtt/config/{configId}` | MQTT配置详情 |
| 8 | `getMqttConfigTableList` | GET | `/api/system/mqtt/config/tableList` | MQTT目标表 |
| 9 | `getMqttConfigTableColumns` | GET | `/api/system/mqtt/config/tableColumns/{tableName}` | MQTT表字段 |
| 10 | `postMqttConfig` | POST | `/api/system/mqtt/config` | 新增MQTT配置 |
| 11 | `putMqttConfig` | PUT | `/api/system/mqtt/config` | 改MQTT配置 |
| 12 | `deleteMqttConfig` | DELETE | `/api/system/mqtt/config/{ids}` | 删MQTT配置 |
| 13 | `getMqttConfigRefreshCache` | GET | `/api/system/mqtt/config/refreshCache` | 刷新缓存 |
| 14 | `getTableAiList` | GET | `/api/datasource/table/ailist` | 表元数据列表 |
| 15 | `testConnection` | POST | `/api/datasource/connection/test` | 测试连接 |
| 16 | `postConnectionDetail` | POST | `/api/datasource/connection` | 创建连接 |
| 17 | `postCreateBuiltinPostgreSQLConnection` | POST | `/api/datasource/connection/create_builtin_postgresql` | 创建内置PG |
| 18 | `postCreateDatabaseTable` | POST | `/api/datasource/connection/{id}/create_database_table` | 创建库+表 |
| 19 | `postCreateTable` | POST | `/api/datasource/connection/{id}/create_table` | 创建表 |
| 20 | `postCreateDatabase` | POST | `/api/datasource/connection/{id}/create_database` | 创建库 |
| 21 | `putAlterDatabase` | PUT | `/api/datasource/connection/{id}/alter_database` | 改库 |
| 22 | `putAlterTable` | PUT | `/api/datasource/connection/{id}/alter_table` | 改表 |
| 23 | `postGenerateTable` | POST | `/api/datasource/connection/generate_table` | AI生成表结构 |
| 24 | `putUpdateBuiltinDatabase` | PUT | `/api/datasource/connection/update_builtin_database` | 改内置PG |
| 25 | `putConnectionDetail` | PUT | `/api/datasource/connection` | 更新连接 |
| 26 | `deleteConnection` | DELETE | `/api/datasource/connection/{id}` | 删连接 |
| 27 | `deleteConnectionConfig` | POST | `/api/datasource/config/deletes` | 删配置 |
| 28 | `putConnectionConfig` | PUT | `/api/datasource/config` | 改库表配置 |
| 29 | `postConnectionConfig` | POST | `/api/datasource/config` | 建库表配置 |
| 30 | `getConnectionRealtimeStructure` | GET | `/api/datasource/connection/realtime/structure/{id}` | 实时库表结构 |
| 31 | `getSkillByDatasource` | GET | `/api/datasource/skill/getByDatasource/{id}` | 按源查技能 |
| 32 | `getSkillBySkillId` | GET | `/api/datasource/skill/getBySkillId/{id}` | 查工具列表 |
| 33 | `postSkillCreateOrGet` | POST | `/api/datasource/skill/createOrGet` | 建技能 |
| 34 | `putSkillUpdateOrGet` | POST | `/api/datasource/skill/updateOrGet` | 改技能 |
| 35 | `postDeleteSkillTool` | DELETE | `/api/datasource/skill/tskilltool/{id}` | 删工具 |
| 36 | `postSkillToolUpdateOrGet` | POST | `/api/datasource/skill/tskilltool/updateOrGet` | 改工具 |
| 37 | `getAiTrainingList` | GET | `/api/ai/training/list` | 训练列表 |
| 38 | `postAiTrainingCreateBySelected` | POST | `/api/ai/training/createBySelected` | 建训练 |
| 39 | `getAiTrainingDetail` | GET | `/api/ai/training/{id}` | 训练详情 |
| 40 | `putConnectionChangeStatus` | PUT | `/api/datasource/connection/changeStatus` | 启停连接 |
| 41 | `getConnectionConfig` | GET | `/api/datasource/config/{id}` | 查源配置 |
| 42 | `getConnectionConfigList` | GET | `/api/datasource/config/list` | 配置列表 |
| 43 | `postSqlSkillConfirmTools` | POST | `/api/datasource/skill/confirmTools` | 确认工具 |
| 44 | `getApiKeyList` | GET | `/api/datasource/api_key/list` | 密钥列表 |
| 45 | `postApiKey` | POST | `/api/datasource/api_key` | 建密钥 |
| 46 | `putApiKey` | PUT | `/api/datasource/api_key` | 改密钥 |
| 47 | `deleteApiKey` | DELETE | `/api/datasource/api_key/{ids}` | 删密钥 |
| 48 | `getApiKeyPermission` | GET | `/api/datasource/api_key/permission/{apiKeyId}` | 查权限 |
| 49 | `postApiKeyPermissionGrantBatch` | POST | `/api/datasource/api_key/permission/grant_batch` | 批量授权 |
| 50 | `postDatasourceSubscriptionToggle` | POST | `/api/datasource/subscription/toggle` | 订阅开关 |
| 51 | `getBuiltinTableData` | GET | `/api/datasource/connection/builtin/table/{tableId}` | 查内置表数据 |
| 52 | `postBuiltinTableRows` | POST | `/api/datasource/connection/builtin/table/{tableId}/rows` | 增行 |
| 53 | `putBuiltinTableRows` | PUT | `/api/datasource/connection/builtin/table/{tableId}/rows` | 改行 |
| 54 | `deleteBuiltinTableRows` | DELETE | `/api/datasource/connection/builtin/table/{tableId}/rows` | 删行 |
| 55 | `getBuiltinTableExportExcel` | GET | `/api/datasource/connection/builtin/table/{tableId}/export/excel` | 导出Excel |
| 56 | `postImportDocumentPreview` | POST | `/api/datasource/connection/{id}/import_document/preview` | 导入预览 |
| 57 | `postImportDocumentConfirm` | POST | `/api/datasource/connection/{id}/import_document/confirm` | 导入确认 |
| 58 | `executeSql` | POST | `/api/datasource/sqlExecutionLog/testSqlWithSchema` | 执行SQL |

View File

@@ -0,0 +1,623 @@
# 数据库管理平台 - MCP 工具设计方案
> 基于平台现有功能,设计可供外部 AI Agent 通过 MCP 协议调用的工具集。
> 目标:**用户脱离平台界面,通过 MCP 调用即可使用数据库管理平台的核心功能。**
---
## 一、设计原则
1. **按用户场景分组**:不是简单映射 API而是围绕用户真实工作流组织工具
2. **最小化调用链**:复杂操作尽量合并为一个 tool减少多轮调用
3. **读写分离**:查询类工具可安全暴露,写操作需明确提示
4. **环境感知**:所有操作默认携带环境参数(prod/test),内置数据源特有
5. **权限前置**:调用方需提供 apiKeyId 或等效凭证,工具内部自动鉴权
---
## 二、工具清单
### 🗄️ 数据源管理
#### 1. `list_datasources`
- **用途**:获取数据源列表,支持搜索和状态筛选
- **对应前端**DataSourceList.vue
- **对应 API**`getConnectionList` ✅ 已实现
- **端点**`GET /api/datasource/connection/list`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| datasourceName | string | 否 | 数据源名称模糊搜索 |
| status | int | 否 | 0=运行中, 1=已停止, 不传=全部 |
| sourceType | string | 否 | builtin/external |
| pageNum | int | 否 | 默认 1 |
| pageSize | int | 否 | 默认 20 |
**返回**:数据源列表(含名称、类型、状态、库数、表数、创建时间等)
---
#### 2. `get_datasource_detail`
- **用途**:获取单个数据源的完整详情
- **对应前端**DatabaseDetail.vue 头部
- **对应 API**
- `getConnectionDetail(id)` ✅ 已实现 — `GET /api/datasource/connection/{id}`
- `getConnectionConfig(id)` ✅ 已实现 — `GET /api/datasource/config/{id}`
- `getConnectionRealtimeStructure(id)` ✅ 已实现 — `GET /api/datasource/connection/realtime/structure/{id}`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| datasourceId | string | 是 | 数据源 ID |
**返回**:数据源详情 + 数据库配置列表 + 实时结构(数据库列表、表列表、字段信息)
---
#### 3. `create_datasource`
- **用途**:创建外部数据源连接
- **对应前端**CreateDataSource.vue
- **对应 API**
- `testConnection(data)` ✅ 已实现 — `POST /api/datasource/connection/test`
- `postConnectionDetail(data)` ✅ 已实现 — `POST /api/datasource/connection`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| datasourceName | string | 是 | 数据源名称3-20字 |
| datasourceType | string | 是 | mysql/postgresql/oracle/sqlserver/dameng |
| host | string | 是 | 数据库地址 |
| port | int | 是 | 端口号 |
| databaseName | string | 是 | 要连接的数据库名 |
| username | string | 是 | 数据库用户名 |
| password | string | 否 | 密码 |
| remark | string | 否 | 数据源描述 |
| connectionType | string | 否 | user_password/ssl默认 user_password |
| test_first | bool | 否 | 是否先测试连接,默认 true |
**返回**:创建结果,含数据源 ID
---
#### 4. `update_datasource`
- **用途**:更新数据源连接信息
- **对应 API**`putConnectionDetail(data)` ✅ 已实现 — `PUT /api/datasource/connection`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | string | 是 | 数据源 ID |
| datasourceName | string | 否 | 更新名称 |
| host | string | 否 | 更新地址 |
| port | int | 否 | 更新端口 |
| databaseName | string | 否 | 更新数据库名 |
| username | string | 否 | 更新用户名 |
| password | string | 否 | 新密码(不传则不变) |
| remark | string | 否 | 更新描述 |
**返回**:更新结果
---
#### 5. `toggle_datasource_status`
- **用途**:启用/停用数据源
- **对应 API**`putConnectionChangeStatus(data)` ✅ 已实现 — `PUT /api/datasource/connection/changeStatus`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | string | 是 | 数据源 ID |
| status | int | 是 | 0=启用, 1=停用 |
**返回**:操作结果
---
#### 6. `delete_datasource`
- **用途**:删除数据源(运行中会自动先停用)
- **对应 API**
- `putConnectionChangeStatus(data)` ✅ 已实现 — `PUT /api/datasource/connection/changeStatus`
- `deleteConnection(id)` ✅ 已实现 — `DELETE /api/datasource/connection/{id}`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | string | 是 | 数据源 ID |
**返回**:删除结果
---
### 📊 数据库与表管理
#### 7. `list_databases`
- **用途**:获取指定数据源下的数据库列表
- **对应 API**`getConnectionConfigList(data)` ✅ 已实现 — `GET /api/datasource/config/list`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| datasourceId | string | 是 | 数据源 ID |
**返回**数据库列表ID、名称、类型、状态、表数
---
#### 8. `list_tables`
- **用途**:获取指定数据库下的表列表
- **对应前端**DatabaseDetail.vue 左侧表列表
- **对应 API**`getTableList(data)` ✅ 已实现 — `GET /api/datasource/table/list`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| datasourceId | string | 是 | 数据源 ID |
| databaseName | string | 否 | 数据库名过滤 |
| keyword | string | 否 | 表名模糊搜索 |
| pageNum | int | 否 | 默认 1 |
| pageSize | int | 否 | 默认 20 |
**返回**:表列表(表名、注释、字段数、创建时间等)
---
#### 9. `get_table_detail`
- **用途**:获取表的完整结构信息
- **对应前端**DatabaseDetail.vue 右侧字段列表
- **对应 API**`getTableDetail(id)` ✅ 已实现 — `GET /api/datasource/table/{id}/detail`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| tableId | string | 是 | 表 ID |
**返回**表名、表注释、字段列表字段名、类型、长度、主键、可空、默认值、注释、AI训练状态
---
#### 10. `create_table`
- **用途**:在指定数据库创建新表
- **对应前端**CreateBuiltinDataSource.vue 表结构编辑器
- **对应 API**`postCreateTable(connectionId, data)` ✅ 已实现 — `POST /api/datasource/connection/{connectionId}/create_table`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| connectionId | string | 是 | 数据源连接 ID路径参数 |
| databaseName | string | 是 | 目标数据库名 |
| tableName | string | 是 | 表名(小写字母+数字+下划线) |
| tableComment | string | 否 | 表注释 |
| columns | array | 是 | 字段定义数组 |
| columns[].columnName | string | 是 | 字段名 |
| columns[].columnType | string | 是 | 字段类型VARCHAR/INTEGER/SERIAL等 |
| columns[].columnLength | int | 否 | 字段长度 |
| columns[].isPrimaryKey | bool | 否 | 是否主键 |
| columns[].isNullable | bool | 否 | 是否可空 |
| columns[].isAutoIncrement | bool | 否 | 是否自增 |
| columns[].columnComment | string | 否 | 字段注释 |
| columns[].defaultValue | string | 否 | 默认值 |
**返回**:创建结果,含表 ID
---
#### 11. `alter_table`
- **用途**:修改已有表结构(增/改/删字段)
- **对应 API**`putAlterTable(connectionId, data)` ✅ 已实现 — `PUT /api/datasource/connection/{connectionId}/alter_table`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| connectionId | string | 是 | 数据源连接 ID路径参数 |
| databaseName | string | 是 | 数据库名 |
| tableName | string | 是 | 表名 |
| operations | array | 是 | 表结构变更操作数组 |
| operations[].operation | string | 是 | ADD_COLUMN / DROP_COLUMN / RENAME_COLUMN / ALTER_COLUMN_TYPE / SET_NOT_NULL / DROP_NOT_NULL / SET_DEFAULT / DROP_DEFAULT |
| operations[].column | object | 是 | 列定义(根据 operation 不同包含不同字段,如 columnName/columnType/newColumnName 等) |
| tableComment | string | 否 | 新表注释 |
**返回**:修改结果
---
#### 12. `generate_table_by_description`
- **用途**:通过自然语言描述让 AI 生成表结构(异步任务)
- **对应前端**CreateBuiltinDataSource.vue AI 生成表结构
- **对应 API**`postGenerateTable(data)` ✅ 已实现 — `POST /api/datasource/connection/generate_table`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| requirement | string | 是 | 业务需求描述 |
| databaseId | int | 否 | 关联的数据库 ID |
**返回**异步任务信息taskId、status需轮询任务状态获取最终生成的表结构
> **场景示例**:用户说"我需要一个商城系统,管理商品、分类和用户评价"AI 返回完整的表结构设计。
---
### 📝 表数据操作 (内置数据源 CRUD)
#### 13. `query_table_data`
- **用途**:查询内置表数据(分页)
- **对应前端**CustomizeDbTable.vue 线上/调试数据视图
- **对应 API**`getBuiltinTableData(tableId, params)` ✅ 已实现 — `GET /api/datasource/connection/builtin/table/{tableId}`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| tableId | string | 是 | 表 ID |
| target | string | 否 | prod/test默认 prod |
| pageNum | int | 否 | 默认 1 |
| pageSize | int | 否 | 默认 10 |
**返回**:表结构信息 + 数据行(二维数组转换后的对象数组) + 总数
---
#### 14. `insert_table_row`
- **用途**:向内置表插入一行数据
- **对应 API**`postBuiltinTableRows(tableId, data, params)` ✅ 已实现 — `POST /api/datasource/connection/builtin/table/{tableId}/rows`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| tableId | string | 是 | 表 ID |
| target | string | 否 | prod/test默认 prod |
| data | object | 是 | 行数据(键值对,键为字段名) |
**返回**:插入结果
---
#### 15. `update_table_row`
- **用途**:更新内置表的指定行
- **对应 API**`putBuiltinTableRows(tableId, data, params)` ✅ 已实现 — `PUT /api/datasource/connection/builtin/table/{tableId}/rows`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| tableId | string | 是 | 表 ID |
| target | string | 否 | prod/test默认 prod |
| primaryKey | object | 是 | 主键值(如 {"id": 1} |
| data | object | 是 | 要更新的字段值 |
**返回**:更新结果
---
#### 16. `delete_table_rows`
- **用途**:删除内置表的指定行
- **对应 API**`deleteBuiltinTableRows(tableId, data, params)` ✅ 已实现 — `DELETE /api/datasource/connection/builtin/table/{tableId}/rows`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| tableId | string | 是 | 表 ID |
| target | string | 否 | prod/test默认 prod |
| primaryKeys | array | 是 | 主键数组(如 [{"id": 1}, {"id": 2}] |
**返回**:删除结果
---
#### 17. `export_table_excel`
- **用途**:导出表数据为 Excel 文件
- **对应 API**`getBuiltinTableExportExcel(tableId, params)` ✅ 已实现 — `GET /api/datasource/connection/builtin/table/{tableId}/export/excel`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| tableId | string | 是 | 表 ID |
| target | string | 否 | prod/test默认 prod |
**返回**Excel 文件(二进制 blob+ 文件名(从 Content-Disposition 解析)
---
### 🔑 API 密钥与权限管理
#### 18. `list_api_keys`
- **用途**:获取 API 密钥列表
- **对应前端**DataSourceKeys.vue
- **对应 API**`getApiKeyList(data)` ✅ 已实现 — `GET /api/datasource/api_key/list`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| apiKeyName | string | 否 | 密钥名称模糊搜索 |
| pageNum | int | 否 | 默认 1 |
| pageSize | int | 否 | 默认 20 |
**返回**密钥列表名称、Key、状态、创建时间
---
#### 19. `create_api_key`
- **用途**:创建新的 API 密钥
- **对应 API**`postApiKey(data)` ✅ 已实现 — `POST /api/datasource/api_key`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| apiKeyName | string | 是 | 密钥名称最多50字 |
**返回**:密钥信息(含 API Key 明文、ID
---
#### 20. `toggle_api_key_status`
- **用途**:启用/禁用 API 密钥
- **对应 API**`putApiKey(data)` ✅ 已实现 — `PUT /api/datasource/api_key`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | string | 是 | 密钥 ID |
| status | int | 是 | 0=启用, 1=禁用 |
**返回**:操作结果
---
#### 21. `delete_api_key`
- **用途**:删除 API 密钥
- **对应 API**`deleteApiKey(ids)` ✅ 已实现 — `DELETE /api/datasource/api_key/{ids}`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | string | 是 | 密钥 ID |
**返回**:删除结果
---
#### 22. `get_api_key_permissions`
- **用途**:查看指定密钥的权限配置
- **对应 API**`getApiKeyPermission(apiKeyId)` ✅ 已实现 — `GET /api/datasource/api_key/permission/{apiKeyId}`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| apiKeyId | string | 是 | 密钥 ID |
**返回**三级权限connectionPermissions / databasePermissions / tablePermissions
---
#### 23. `grant_api_key_permissions`
- **用途**:批量为 API 密钥授予权限
- **对应前端**DataSourceKeySetting.vue
- **对应 API**`postApiKeyPermissionGrantBatch(data)` ✅ 已实现 — `POST /api/datasource/api_key/permission/grant_batch`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| apiKeyId | string | 是 | 密钥 ID |
| batchDatas | array | 是 | 权限批量数据数组 |
| batchDatas[].connectionId | string | 是 | 数据源 ID |
| batchDatas[].permissionLevel | string | 是 | connection/database/table |
| batchDatas[].permissionType | string | 是 | 权限类型(逗号分隔) |
| batchDatas[].databaseName | string | 否 | 数据库名level=database/table 时) |
| batchDatas[].tableName | string | 否 | 表名level=table 时) |
**返回**:授权结果
---
#### 23.5 ~~`revoke_api_key_permissions`~~(已废弃,不提供)
- **结论**:权限为「仅追加」模型。真机验证后端 permission 删除接口返回「不支持当前的调用方式」,无法撤销单条已授予权限,故不实现该工具。
- **替代方案**:要缩小某密钥的权限范围,只能「删密钥(`delete_api_key`)→ 重建(`create_api_key`)→ 重新授权(`grant_api_key_permissions`)」。
---
### 🤖 技能与工具管理 (内置数据源 AI 能力)
#### 24. `get_skill_by_datasource`
- **用途**:根据数据源获取技能信息
- **对应 API**`getSkillByDatasource(id)` ✅ 已实现 — `GET /api/datasource/skill/getByDatasource/{id}`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| datasourceId | string | 是 | 数据源 ID |
**返回**:技能信息(含 skillBool 标识)
---
#### 25. `get_skill_tools`
- **用途**:获取技能下的工具列表
- **对应 API**`getSkillBySkillId(id)` ✅ 已实现 — `GET /api/datasource/skill/getBySkillId/{id}`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| skillId | string | 是 | 技能 ID |
**返回**工具列表名称、描述、SQL模板、参数定义、业务场景
---
#### 26. ~~`create_skill`~~(已移除,不再单独暴露)
- **结论**:技能(skill)必须挂着工具才有效,平时不会单独创建技能;单独建技能会留下无效的空技能。前端也没有「只建技能」入口。
- **替代方案**:把 SQL 沉淀为工具统一用 `add_sql_tool_to_datasource`(见 §29.5),它内部按需调 `skill/createOrGet` 建技能、写配置模板、再 `confirmTools` 建工具,一步到位且保证技能必有工具。底层 `skill/createOrGet` 端点仍被该编排工具内部使用,只是不再作为独立 MCP 工具暴露。
---
#### 27. `create_sql_tool`
- **用途**:将 SQL 查询创建为可复用工具
- **对应前端**SqlControllerMsg.vue 添加到工具功能
- **对应 API**`postSqlSkillConfirmTools(data)` ✅ 已实现 — `POST /api/datasource/skill/confirmTools`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| skillId | string | 是 | 技能 ID |
| tableIds | array | 否 | 关联的表 ID 数组 |
| suggestions | array | 是 | SQL 工具建议数组(支持批量) |
| suggestions[].name | string | 是 | 工具名称 |
| suggestions[].businessDescription | string | 是 | 业务描述 |
| suggestions[].sqlTemplate | string | 是 | SQL 模板(支持 #{param} 参数占位) |
| suggestions[].sqlParams | string/object | 否 | 参数 JSON Schema对象会自动序列化为字符串 |
| suggestions[].resultType | string | 否 | single/list默认 list |
| suggestions[].businessScenario | string | 否 | 业务场景描述 |
**返回**:工具创建结果
---
#### 28. `delete_skill_tool`
- **用途**:删除技能下的工具
- **对应 API**`postDeleteSkillTool(skillToolId)` ✅ 已实现 — `DELETE /api/datasource/skill/tskilltool/{skillToolId}`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| skillToolId | string | 是 | 工具 ID |
**返回**:删除结果
---
#### 29. `update_skill_config`
- **用途**:更新技能配置(如 MCP Server 配置模板)
- **对应 API**`putSkillUpdateOrGet(data)` ✅ 已实现 — `POST /api/datasource/skill/updateOrGet`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| datasourceId | string | 是 | 数据源 ID |
| configTemplate | string | 是 | 配置模板 JSON 字符串 |
**返回**:更新结果
---
### 📥 数据导入
#### 30. `preview_import_data`
- **用途**:上传 Excel 文件AI 智能识别并预览表结构/数据
- **对应前端**TableRecognition.vue
- **对应 API**`postImportDocumentPreview(connectionId, file, target)` ✅ 已实现 — `POST /api/datasource/connection/{connectionId}/import_document/preview`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| connectionId | int | 是 | 数据源 ID |
| target | string | 否 | prod/test默认 test |
| file | binary | 是 | Excel 文件 (.xlsx/.xls, <500KB) |
**返回**:识别结果(表结构 + 数据预览)
---
#### 31. `confirm_import_data`
- **用途**:确认导入 AI 识别后的数据
- **对应 API**`postImportDocumentConfirm(connectionId, data, params)` ✅ 已实现 — `POST /api/datasource/connection/{connectionId}/import_document/confirm`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| connectionId | int | 是 | 数据源 ID |
| databaseName | string | 是 | 落库目标库名 |
| target | string | 否 | prod/test |
| data | object | 是 | 导入数据(含 tableStructure + allData |
**data.tableStructure.columns 的关键约束**
- `columns` 定义了「这批数据要写入哪些字段」,**导入时这些列必须对应目标表中真实存在的字段**——列名(`columnName`)要与目标表的实际字段名一致,否则后端按列名拼 INSERT 时会报「查询字段不存在 / 字段名称不正确」。
- `allData` 的**首行是列名表头**= `columns[].columnName`),数据从第 2 行起;每行按 `columns` 顺序给出**全部列**的值(全列宽,不裁剪)。表头列名同样必须是目标表存在的字段。
- **导入到已有表时**:前端会用目标表的真实列(`get_table_detail` 返回的 columns覆盖 AI 识别的列。MCP 调用方应等价处理——**先 `get_table_detail` 拿到目标表真实字段,再让 data 里的 columns / 表头 / 数据与之对齐**,不要直接用 Excel 识别出的、可能与表字段不符的列。
**返回**:导入结果
---
### 🏷️ 表订阅
#### 32. `toggle_table_subscription`
- **用途**:切换表的订阅状态
- **对应前端**DatabaseDetail.vue 订阅按钮
- **对应 API**`postDatasourceSubscriptionToggle(data)` ✅ 已实现 — `POST /api/datasource/subscription/toggle`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| tableId | string | 是 | 表 ID |
| datasourceId | string | 是 | 数据源 ID |
| subscribe | bool | 是 | true=订阅, false=取消订阅 |
**返回**:操作结果
---
### 🔧 SQL 执行
#### 33. `execute_sql`
- **用途**:执行原生 SQL 查询
- **对应 API**`executeSql(data)` ✅ 已实现 — `POST /api/datasource/sqlExecutionLog/testSqlWithSchema`
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| datasourceId | string | 是 | 数据源 ID |
| sql | string | 是 | SQL 语句 |
| target | string | 否 | prod/test默认 prod |
| params | object | 否 | 参数对象 |
**返回**:查询结果(表头、数据行、业务名称、描述等)
---
## 三、推荐使用场景
### 场景 1外部 AI Agent 管理数据库
```
用户: "帮我查一下有哪些数据源"
Agent: 调用 list_datasources()
用户: "看看 mall_db 有哪些表"
Agent: 调用 list_tables(datasourceId="xx", databaseName="mall_db")
用户: "users 表结构给我看一下"
Agent: 调用 get_table_detail(tableId="xx")
```
### 场景 2通过 AI 描述自动生成表结构
```
用户: "我需要一个订单系统,包含订单、订单明细、支付方式"
Agent: 调用 generate_table_by_description(description="我需要一个订单系统...")
Agent: 返回 AI 生成的表结构,用户确认后调用 create_table() 批量创建
```
### 场景 3管理 API 密钥和权限
```
用户: "帮我创建一个叫'第三方报表系统'的 API Key"
Agent: 调用 create_api_key(apiKeyName="第三方报表系统")
用户: "给它开通 mall_db 数据库的读取权限"
Agent: 调用 grant_api_key_permissions(apiKeyId="xx", batchDatas=[{...}])
```
### 场景 4管理表数据
```
用户: "查一下 users 表前 10 条数据"
Agent: 调用 query_table_data(tableId="xx", pageNum=1, pageSize=10)
用户: "新增一个用户,用户名是 test_user"
Agent: 调用 insert_table_row(tableId="xx", data={"user_name": "test_user", ...})
```
### 场景 5创建 SQL 工具
```
用户: "把这个查询保存为工具,叫'按地区统计订单'"
Agent: 调用 create_sql_tool(skillId="xx", name="按地区统计订单",
sqlTemplate="SELECT region, COUNT(*) FROM orders GROUP BY region", ...)
```
### 场景 6导入 Excel 数据
```
用户: "帮我导入这份 Excel 到测试环境"
Agent: 调用 preview_import_data(connectionId=xx, target="test", file=...)
Agent: 展示识别结果,用户确认后调用 confirm_import_data(...)
```
---
## 四、工具优先级建议
| 优先级 | 工具 | 理由 |
|--------|------|------|
| **P0 核心** | list_datasources, list_tables, get_table_detail, execute_sql, query_table_data | 覆盖 80% 查询场景 |
| **P1 常用** | create_datasource, create_table, generate_table_by_description, create_api_key, grant_api_key_permissions | 管理核心操作 |
| **P2 扩展** | insert/update/delete_table_row, export_table_excel, create_sql_tool, toggle_table_subscription | 数据操作与 AI 能力 |
| **P3 完整** | preview/confirm_import_data, update_skill_config, alter_table, delete_skill_tool | 高级功能 |
---
## 五、实现建议
1. **MCP Server 实现**:建议用 Python + `mcp` 库或 Node.js + `@modelcontextprotocol/sdk`
2. **鉴权方式**:工具内部复用平台现有的 API Key 鉴权机制,调用方传入 apiKeyId
3. **错误处理**:统一错误格式 `{ success: false, error: "描述" }`,与平台 API 拦截器保持一致
4. **环境默认值**:所有 target 参数默认 prod调用方显式指定 test 才能操作测试数据
5. **批量操作**:对于 create_table 等可能涉及多表的场景,支持批量参数或循环调用
6. **AI 工具调用**generate_table_by_description 等 AI 工具需要调用平台的 AI 服务接口agent generic / aiTextGenerator
7. **前端 API 映射**:所有 33 个 MCP 工具均已在前端 `src/server/database.ts` 中实现对应函数,可直接复用

View File

@@ -0,0 +1,303 @@
# 数据库管理平台 - 功能总览
> 本文档梳理 `databasePage` 组件集群与 `DatabaseDetailPage` 的整体功能架构。
---
## 一、系统定位
这是一个面向业务用户的**数据库管理与智能问数一体化平台**,支持两种数据源模式:
| 模式 | 说明 | 核心差异 |
|------|------|----------|
| **外部数据源** | 远程 MySQL/PostgreSQL/Oracle/SQL Server/达梦 等 | 标准数据库连接,提供只读/CRUD 管理 |
| **内置数据源** | 平台内建 PostgreSQL 实例 | 支持 AI 技能、工具、环境切换(prod/test)、AI 训练 |
---
## 二、功能模块全景
### 1. 数据源管理
#### 1.1 数据源列表 (`DataSourceList.vue`)
- **卡片式展示**名称、ID、类型徽章(内置/外部)、数据库类型、运行状态、库数/表数统计
- **无限滚动分页**IntersectionObserver + useInfiniteScroll每页 20 条
- **搜索与筛选**:按名称模糊搜索、按状态筛选(运行中/已停止)
- **数据源操作**
- 编辑(外部→CreateDataSource, 内置→CreateBuiltinDataSource)
- 启用/停用(切换 status 0/1)
- 删除(运行中自动先停用再删除)
- **业务执行入口**:点击打开智能问数聊天界面 (ChatDebugging)
- **查看详情入口**:右侧 Drawer 打开 DatabaseDetail (95% 宽度)
#### 1.2 外部数据源创建/编辑 (`CreateDataSource.vue`) - 4步向导
| 步骤 | 内容 | 核心功能 |
|------|------|----------|
| Step 1 | 选择数据库类型 | MySQL/PostgreSQL/Oracle/SQL Server/达梦,自动填充默认端口 |
| Step 2 | 配置连接信息 | 名称、描述(AI润色)、主机、端口、认证(基础认证/SSL)、用户名密码(AI生成名称)、测试连接 |
| Step 3 | 选择数据库和表 | 树形展开、多选、搜索过滤、分页加载表列表、全选/清空 |
| Step 4 | 确认信息 | 汇总展示已选数据库和表标签 |
#### 1.3 内置数据源创建/编辑/添加表 (`CreateBuiltinDataSource.vue`)
| 模式 | 步骤 | 说明 |
|------|------|------|
| create | 名称描述→业务场景→表结构 | AI 生成表结构,批量创建数据库和表 |
| edit | 名称描述→表结构 | 更新已有内置数据源 |
| addTable | 业务场景→表结构 | 在已有内置数据源下新增表 |
- **AI 能力**AI 优化描述、AI 生成数据库名、AI 生成表名/表描述/字段描述
- **表结构编辑器**:左右分栏(左侧表列表, 右侧 TableDetailEditor 预览/编辑)
- **提交流程**:创建数据源 → 创建 PostgreSQL 连接 → 创建数据库 → 批量建表 → 检查 AI 训练状态
### 2. 数据库详情 (`DatabaseDetail.vue`)
左侧面板 + 右侧面板的经典布局:
#### 2.1 左侧 - 数据库与表管理
- **数据库选择器**:下拉切换当前数据库,悬浮显示数据库详情(ID/名称/数据源/类型)
- **数据表列表**
- 搜索过滤
- 多选/全选 (Checkbox)
- 无限滚动加载
- AI 补全(选中多个表批量触发)
- 更多操作: AI智能建表、智能导入表、删除库
- **底部功能按钮**字段关联管理、AI补全管理
#### 2.2 右侧 - 详情视图(三种视图)
**视图 A: 表详情 (`fieldViewMode = fields`)**
- 字段列表表格: 字段名、类型、长度、可空、主键、默认值、注释、描述
- AI 训练状态徽章(已训练/未训练)
- 操作: 修改表、字段关联管理、表订阅(已订阅/未订阅)、刷新
- 用户 ID 字段关联提示(user_id/createById 等自动关联系统用户)
**视图 B: 线上数据 (`fieldViewMode = online`)**
- 通过 `CustomizeDbTable` 组件查看/编辑生产环境真实数据
- 支持增删改查、导出 Excel、智能导入数据
**视图 C: 调试数据 (`fieldViewMode = debug`)**
- 同上,但操作测试环境数据
#### 2.3 右侧 - 字段关联管理视图
- 为当前表的字段建立与目标表字段的映射关系
- 目标表数据更新时自动同步到当前表
- 支持保存配置、添加/删除关联
#### 2.4 右侧 - AI 补全管理视图
- AI 补全任务列表: 任务名、类型、状态(待执行/运行中/成功/失败)、进度
- 任务详情弹窗: 训练结果(格式化代码展示)、耗时、场景描述
- 支持重新执行、删除任务
### 3. 内建表数据管理 (`CustomizeDbTable.vue`)
- 基于 `CommonDbTable` 的完整 CRUD 表格
- 动态列配置: 根据表结构自动生成字段类型、宽度、可编辑性
- **操作能力**: 新增行、编辑行、删除行、导出 Excel、导入数据、刷新
- **导入流程**: 下载模板 → 上传 Excel → AI 识别预览 → 确认导入(调用 postImportDocumentConfirm)
- 环境切换: `target` prop 控制 prod/test
### 4. 智能导入识别 (`TableRecognition.vue`)
- **Step 1: 上传文件**: 选择目标环境(prod/test),上传 Excel (.xlsx/.xls, <500KB)
- **Step 2: 智能识别**: AI 解析 Excel预览表结构/数据,支持编辑
- **两种模式**:
- `table` 模式: 根据 Excel 内容自动生成数据表结构
- `data` 模式: 根据规范模板识别并导入数据
- 空行过滤、字段标记、模板下载
### 5. 智能问数 / 业务执行 (`ChatDebugging.vue`)
- **三栏布局**: 左侧面板(可折叠) + 中间聊天区 + 右侧信息
- **左侧面板**:
- 数字员工列表(可收起)
- 新建会话按钮
- 会话列表(点击切换、删除)
- 技能列表(展开/折叠)
- 技能名称 + 编辑按钮
- 工具列表(名称、别名、编辑、删除)
- 至少保留一个工具
- **中间聊天区** (`ChatBusiness` 组件):
- 数据源插件嵌入输入区 (`DataSourcePlugIn`)
- 数据库下拉选择
- 环境切换(生产/测试,仅内置数据源)
- SQL 消息控制器 (`SqlControllerMsg`)
- 默认数据库查询提示词(常用查询/数据统计/新手指引)
#### 5.1 SQL 查询结果展示 (`SqlControllerMsg.vue`)
| 视图类型 | 说明 |
|----------|------|
| SQL 代码 | 展示可执行 SQL 语句,支持复制 |
| 图表可视化 | ChartGallery 组件渲染图表 |
| 查询数据 | JSON 格式原始数据,支持复制 |
| 文本说明 | 业务名称、描述、AI 解释 |
| 表格视图 | Ant Design Table 展示查询结果 |
| 添加到工具 | 将 SQL 查询保存为可复用工具 |
- **添加工具功能**:
- 重复检测(按 sqlTemplate 归一化比对)
- 仅生产环境可用
- 技能创建/更新流程(无技能时自动创建 skill + MCP 配置)
- 通过 EventBus 触发技能刷新
#### 5.2 数据源选择插件 (`DataSourcePlugIn.vue`)
- 紧凑的内联选择器: 数据源名称 → 数据库下拉 → 环境(prod/test)
- 监听 props 变化自动重置无效选择
- 暴露 getSelection/clearSelection/setEnvironment 方法
### 6. API 密钥管理 (`DataSourceKeys.vue` + `DataSourceKeySetting.vue`)
- **密钥列表**: 名称、API Key(脱敏显示/可切换显示)、状态开关(启用/禁用)、创建时间、操作(详情/编辑/删除)
- **两步创建流程**:
1. 输入密钥名称 → 调用 postApiKey 创建
2. 弹窗询问是否配置权限 → 打开权限配置
- **权限配置** (`DataSourceKeySetting.vue`) - 3步向导:
- Step 1: 选择数据源(单选) + 配置数据源级权限
- Step 2: 选择数据库(单选, 非必填) + 配置数据库级权限
- Step 3: 选择数据表(多选, 非必填) + 行内配置表级权限
- Step 4: 完成页(ApiKeyPermissionPreview 汇总) → 批量提交权限
- **权限选项**: 按层级不同(connection/database/table),每种有对应的读/写/管理等权限
- **API 调用文档**: 切换到文档视图查看调用说明
### 7. 表字段编辑器 (`TableDetailEditor.vue`)
- **表信息编辑**: 表名(AI生成)、表描述(AI优化)
- **字段列表表格**:
- 序号、字段名、字段类型(分组下拉)、长度、主键、自增、可空、默认值、注释
- AI 生成字段描述(逐字段)
- 添加字段(首个字段自动设为主键+自增)
- 删除字段(有id的字段记录到 deletedColumns)
- 主键设置后不可取消(已有字段)
- 自增字段自动切换为整数类型
- **两种模式**: `edit` (编辑) / `preview` (预览)
- **校验功能**: validateFields 供外部调用
- **字段类型** (面向 PostgreSQL): VARCHAR/TEXT/SERIAL/INTEGER/BIGINT/NUMERIC/TIMESTAMP/BOOLEAN/JSONB/INT2/INT4/INT8/SMALLINT/SMALLSERIAL/BIGSERIAL/BOOL/BIGSERIAL
### 8. 其他辅助组件
| 组件 | 功能 |
|------|------|
| `ChartGallery.vue` | 图表可视化渲染 |
| `AddToolModal.vue` | 添加工具弹窗(工具名、描述编辑+AI辅助) |
| `ApiKeyPermissionPreview.vue` | 权限汇总预览组件 |
| `ApiCallDocument.vue` | API 调用文档展示 |
| `DatabaseMessageList.vue` | 数据库消息列表 |
| `field-types-dictionary.json` | 字段类型字典 |
---
## 三、API 接口层 (server/database.ts)
### 数据源管理
| 函数 | 方法 | 用途 |
|------|------|------|
| `getConnectionList` | GET | 数据源列表(分页+搜索+筛选) |
| `getConnectionDetail` | GET | 数据源详情 |
| `postConnectionDetail` | POST | 创建数据源 |
| `putConnectionDetail` | PUT | 更新数据源 |
| `deleteConnection` | DELETE | 删除数据源 |
| `putConnectionChangeStatus` | PUT | 切换数据源状态(启用/停用) |
| `testConnection` | POST | 测试连接 |
### 内置数据库管理
| 函数 | 方法 | 用途 |
|------|------|------|
| `postCreateBuiltinPostgreSQLConnection` | POST | 创建内置 PostgreSQL 连接 |
| `postCreateDatabase` | POST | 创建数据库 |
| `putAlterDatabase` | PUT | 修改数据库 |
| `postCreateTable` | POST | 创建表 |
| `putAlterTable` | PUT | 修改表 |
| `postGenerateTable` | POST | AI 生成表结构 |
| `putUpdateBuiltinDatabase` | PUT | 更新内置数据库 |
### 数据库配置
| 函数 | 方法 | 用途 |
|------|------|------|
| `getConnectionConfig` | GET | 获取数据库配置(含 skillBool) |
| `getConnectionConfigList` | GET | 数据库配置列表 |
| `postConnectionConfig` | POST | 创建数据库配置 |
| `putConnectionConfig` | PUT | 更新数据库配置 |
| `deleteConnectionConfig` | DELETE | 删除数据库配置 |
| `getConnectionRealtimeStructure` | GET | 获取实时数据库结构 |
### 表数据管理
| 函数 | 方法 | 用途 |
|------|------|------|
| `getTableList` | GET | 表列表 |
| `getTableDetail` | GET | 表详情 |
| `getBuiltinTableData` | GET | 获取内置表数据(分页) |
| `postBuiltinTableRows` | POST | 新增表数据行 |
| `putBuiltinTableRows` | PUT | 更新表数据行 |
| `deleteBuiltinTableRows` | DELETE | 删除表数据行 |
| `getBuiltinTableExportExcel` | GET | 导出表数据为 Excel |
### 数据导入
| 函数 | 方法 | 用途 |
|------|------|------|
| `postImportDocumentPreview` | POST | 上传 Excel 预览识别结果 |
| `postImportDocumentConfirm` | POST | 确认导入数据 |
### 技能与工具
| 函数 | 方法 | 用途 |
|------|------|------|
| `getSkillByDatasource` | GET | 根据数据源获取技能 |
| `getSkillBySkillId` | GET | 根据技能ID获取工具列表 |
| `postSkillCreateOrGet` | POST | 创建或获取技能 |
| `putSkillUpdateOrGet` | PUT | 更新技能配置 |
| `postSkillToolUpdateOrGet` | POST | 创建/更新技能工具 |
| `postDeleteSkillTool` | POST | 删除技能工具 |
| `postSqlSkillConfirmTools` | POST | 确认并创建 SQL 工具 |
### AI 训练
| 函数 | 方法 | 用途 |
|------|------|------|
| `getAiTrainingList` | GET | AI 训练任务列表 |
| `postAiTrainingCreateBySelected` | POST | 创建 AI 训练任务 |
| `getAiTrainingDetail` | GET | 训练任务详情 |
### API 密钥
| 函数 | 方法 | 用途 |
|------|------|------|
| `getApiKeyList` | GET | 密钥列表 |
| `postApiKey` | POST | 创建密钥 |
| `putApiKey` | PUT | 更新密钥 |
| `deleteApiKey` | DELETE | 删除密钥 |
| `getApiKeyPermission` | GET | 获取密钥权限 |
| `postApiKeyPermissionGrantBatch` | POST | 批量授予权限 |
### 订阅
| 函数 | 方法 | 用途 |
|------|------|------|
| `postDatasourceSubscriptionToggle` | POST | 切换表订阅状态 |
### SQL 执行
| 函数 | 方法 | 用途 |
|------|------|------|
| `executeSql` | POST | 执行 SQL 查询 |
---
## 四、状态管理与通信
- **Pinia Store**: `useDatabaseChatStore` 管理选中数据库、环境、技能状态
- **EventBus**: `EVENTS.RELOAD_SKILL_DATA` 用于工具添加后刷新技能数据
- **权限控制**: `hasPermission()` 函数按权限标识控制按钮可见性
## 五、权限标识体系
| 权限标识 | 控制范围 |
|----------|----------|
| `database:create` | 创建数据源 |
| `database:edit` | 编辑数据源 |
| `database:delete` | 删除数据源 |
| `database:import` | 导入表/数据 |
| `database:export` | 导出表 |
| `database:table:create` | AI 智能建表 |
| `database:table:ai:complete` | AI 补全功能 |
| `database:apikey:view` | 查看密钥管理 |
| `database:apikey:create` | 创建密钥 |
---
## 六、关键技术特点
1. **AI 深度集成**: 描述润色、名称生成、表结构自动生成、字段描述生成、AI 补全、AI 训练
2. **双环境隔离**: 生产/测试环境切换,测试环境限制添加工具等敏感操作
3. **无限滚动**: IntersectionObserver + 哨兵元素实现列表分页加载
4. **三级权限体系**: 数据源 → 数据库 → 数据表,通过 API Key 控制访问粒度
5. **MCP 工具集成**: 内置数据源自动配置 MCP Server (`lzwcai-mcp-sqlexecutor`),支持 AI 工具注册
6. **Excel 智能导入**: AI 识别 Excel 内容,自动匹配表结构,支持表结构和数据两种导入模式

View File

@@ -0,0 +1,14 @@
"""
Entry point for lzwcai-mcp-agile-db
Runs the MCP server for database management platform
"""
from lzwcai_mcp_agile_db.server import main
import os
if __name__ == "__main__":
# 账号密码方式:客户端会在首次请求时自动调用 /login 换取 token
os.environ["AGILE_DB_ACCOUNT"] = "yy8z9"
os.environ["AGILE_DB_PASSWORD"] = "lzwc@2025."
os.environ["backendBaseUrl"] = "http://192.168.2.236:8082/api"
main()

View File

@@ -0,0 +1,34 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "lzwcai-mcp-agile-db"
version = "0.1.17"
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"
license = {text = "MIT"}
authors = [
{name = "lzwcai", email = "your-email@example.com"},
]
keywords = ["mcp", "database", "agile", "server"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"httpx>=0.28.1",
"mcp[cli]>=1.10.1",
]
[project.scripts]
lzwcai-mcp-agile-db = "lzwcai_mcp_agile_db.server:main"
[tool.hatch.build.targets.wheel]
packages = ["lzwcai_mcp_agile_db"]

View File

@@ -0,0 +1 @@
3.12

View File

@@ -0,0 +1,75 @@
# lzwcai-mcp-agile-db-third
AgileDB 数据源管理 MCP Server基于 `API_DOCUMENTATION.md` 将后端数据源/连接/DDL/DML/API 接口封装为 34 个 MCP 工具。
## 功能概述
本服务将后端 `datasource` 模块的 API 接口代理为标准 MCP 工具,分为两大类:
### 1. 数据源配置管理
- `list_datasource_configs`:查询数据源配置列表
- `get_datasource_config`:获取数据源配置详情
- `batch_create_datasource_configs`:批量创建数据源配置
- `replace_datasource_configs`:全量替换数据源配置
- `batch_update_datasource_configs`:批量修改数据源配置
- `delete_datasource_configs`:批量删除数据源配置
- `test_connection_config`:测试数据库连接
- `change_datasource_status`:修改数据源状态
- `export_datasource_configs`:导出数据源配置为 Excel
### 2. 数据库连接实例管理
- `list_connections` / `get_connection` / `create_connection` / `update_connection` / `delete_connection`:连接实例 CRUD
- `test_connection` / `change_connection_status`:连接测试与状态切换
- `realtime_structure` / `realtime_databases` / `realtime_tables`:实时查询库表结构
- `create_builtin_postgresql` / `update_builtin_database`:内置 PostgreSQL 连接管理
- `execute_sql`:执行原生 SQL
- `create_database` / `create_table` / `create_database_table` / `alter_database` / `alter_table`DDL 操作
- `generate_table`AI 生成表结构
- `import_document_preview` / `import_document_confirm`Excel/CSV 文档导入
- `builtin_table_data` / `builtin_table_insert` / `builtin_table_update` / `builtin_table_delete`:表数据 CRUD
## 环境配置
| 环境变量 | 说明 | 默认值 |
|----------|------|--------|
| `backendBaseUrl` | 后端 API 基础地址 | `http://lzwcai-demp-corp-manager:8086` |
| `datasourceApiKey` | 默认 `X-Datasource-API-Key`,可选 | 空 |
| `LOG_LEVEL` | 日志级别 | `INFO` |
## 安装与运行
使用 uv
```bash
uv sync
uv run python -m lzwcai_mcp_agile_db_third.main
```
或使用 pip
```bash
python -m venv .venv
.venv\Scripts\python -m pip install mcp httpx
.venv\Scripts\python -m lzwcai_mcp_agile_db_third.main
```
安装为命令后:
```bash
lzwcai-mcp-agile-db-third
```
## 使用 mcp CLI
```bash
mcp dev lzwcai_mcp_agile_db_third/main.py
```
## 注意事项
- 所有写操作(创建/修改/删除)会先落到 `prod` 环境;带 `target` 参数的工具可切换为 `test`
- `import_document_preview` 通过本地文件路径上传 Excel/CSV
- 执行删除类工具前,调用方应遵循安全确认原则,向用户展示影响范围并二次确认
- 日志文件中会对 `password``apiKey``token``secret` 等敏感字段进行脱敏

View File

@@ -0,0 +1,21 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
*.egg
# Virtual environments
.venv/
venv/
# Logs
logs/
*.log
# IDE
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
# lzwcai-mcp-agile-db-third
AgileDB 数据源管理 MCP Server基于 `API_DOCUMENTATION.md` 将后端数据源/连接/DDL/DML/API 接口封装为 34 个 MCP 工具。
## 功能概述
本服务将后端 `datasource` 模块的 API 接口代理为标准 MCP 工具,分为两大类:
### 1. 数据源配置管理
- `list_datasource_configs`:查询数据源配置列表
- `get_datasource_config`:获取数据源配置详情
- `batch_create_datasource_configs`:批量创建数据源配置
- `replace_datasource_configs`:全量替换数据源配置
- `batch_update_datasource_configs`:批量修改数据源配置
- `delete_datasource_configs`:批量删除数据源配置
- `test_connection_config`:测试数据库连接
- `change_datasource_status`:修改数据源状态
- `export_datasource_configs`:导出数据源配置为 Excel
### 2. 数据库连接实例管理
- `list_connections` / `get_connection` / `create_connection` / `update_connection` / `delete_connection`:连接实例 CRUD
- `test_connection` / `change_connection_status`:连接测试与状态切换
- `realtime_structure` / `realtime_databases` / `realtime_tables`:实时查询库表结构
- `create_builtin_postgresql` / `update_builtin_database`:内置 PostgreSQL 连接管理
- `execute_sql`:执行原生 SQL
- `create_database` / `create_table` / `create_database_table` / `alter_database` / `alter_table`DDL 操作
- `generate_table`AI 生成表结构
- `import_document_preview` / `import_document_confirm`Excel/CSV 文档导入
- `builtin_table_data` / `builtin_table_insert` / `builtin_table_update` / `builtin_table_delete`:表数据 CRUD
## 环境配置
| 环境变量 | 说明 | 默认值 |
|----------|------|--------|
| `backendBaseUrl` | 后端 API 基础地址 | `http://lzwcai-demp-corp-manager:8086` |
| `datasourceApiKey` | 默认 `X-Datasource-API-Key`,可选 | 空 |
| `LOG_LEVEL` | 日志级别 | `INFO` |
## 安装与运行
使用 uv
```bash
uv sync
uv run python -m lzwcai_mcp_agile_db_third.main
```
或使用 pip
```bash
python -m venv .venv
.venv\Scripts\python -m pip install mcp httpx
.venv\Scripts\python -m lzwcai_mcp_agile_db_third.main
```
安装为命令后:
```bash
lzwcai-mcp-agile-db-third
```
## 使用 mcp CLI
```bash
mcp dev lzwcai_mcp_agile_db_third/main.py
```
## 注意事项
- 所有写操作(创建/修改/删除)会先落到 `prod` 环境;带 `target` 参数的工具可切换为 `test`
- `import_document_preview` 通过本地文件路径上传 Excel/CSV
- 执行删除类工具前,调用方应遵循安全确认原则,向用户展示影响范围并二次确认
- 日志文件中会对 `password``apiKey``token``secret` 等敏感字段进行脱敏

View File

@@ -0,0 +1 @@
"""lzwcai_mcp_agile_db_third package."""

View File

@@ -0,0 +1,216 @@
"""MCP server entrypoint for Agile DB third-party datasource APIs."""
import asyncio
import json
import logging
from typing import Any, Dict, List, Optional
try:
from .tools import list_tools, get_tool
from .utils import DataSourceAPIClient, get_env_config, get_api_key
from .utils.logger_config import logger_config
except ImportError:
from tools import list_tools, get_tool
from utils import DataSourceAPIClient, get_env_config, get_api_key
from utils.logger_config import logger_config
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
import mcp.types as types
mcp_logger = logger_config.setup_mcp_logging()
def _text_response(payload: Dict[str, Any]) -> List[types.TextContent]:
"""Build a JSON text response."""
return [
types.TextContent(
type="text",
text=json.dumps(payload, ensure_ascii=False, indent=2),
)
]
def _build_tool(tool_def: Dict[str, Any]) -> types.Tool:
"""Build an MCP Tool from a tool definition."""
return types.Tool(
name=tool_def["name"],
description=tool_def["description"],
inputSchema=tool_def["inputSchema"],
)
server = Server("lzwcai-mcp-agile-db-third")
_api_client: Optional[DataSourceAPIClient] = None
def _get_api_client() -> DataSourceAPIClient:
"""Get or create the default API client."""
global _api_client
if _api_client is None:
env = get_env_config()
_api_client = DataSourceAPIClient(base_url=env.get("backend_base_url"))
return _api_client
@server.list_tools()
async def handle_list_tools() -> List[types.Tool]:
"""List all available MCP tools."""
try:
mcp_logger.info("收到列出工具请求")
tools = [_build_tool(tool_def) for tool_def in list_tools()]
mcp_logger.info(f"成功生成 {len(tools)} 个 MCP 工具")
return tools
except Exception as e:
mcp_logger.error(f"列出工具失败: {e}", exc_info=True)
raise
@server.call_tool()
async def handle_call_tool(
name: str,
arguments: Optional[Dict[str, Any]],
) -> List[types.TextContent]:
"""Handle MCP tool invocation by proxying to the backend API."""
try:
mcp_logger.info(f"收到工具调用请求: {name}")
mcp_logger.debug(f"工具参数: {arguments}")
tool_def = get_tool(name)
if tool_def is None:
error_msg = f"未找到工具: {name}"
mcp_logger.warning(error_msg)
return _text_response({"error": error_msg})
# Validate required parameters
args = arguments or {}
schema = tool_def["inputSchema"]
required = schema.get("required", [])
missing = [key for key in required if key not in args or args[key] is None]
if missing:
error_msg = f"工具 {name} 缺少必填参数: {', '.join(missing)}"
mcp_logger.warning(error_msg)
return _text_response({"error": error_msg, "missing": missing})
# Categorize parameters
path_params: Dict[str, Any] = {}
query_params: Dict[str, Any] = {}
body_params: Dict[str, Any] = {}
file_path: Optional[str] = None
api_key: Optional[str] = None
categories = tool_def["paramCategories"]
for key, value in args.items():
if value is None:
continue
category = categories.get(key)
if category == "path":
path_params[key] = value
elif category == "query":
query_params[key] = value
elif category == "body":
body_params[key] = value
elif category == "file":
file_path = value
elif category == "header":
api_key = value
# Apply schema-defined defaults for parameters the caller omitted,
# so "有默认值的参数不填就用默认值"(如 datasourceType、target、分页
properties = schema.get("properties", {})
for key, prop in properties.items():
if "default" not in prop:
continue
if args.get(key) is not None:
continue
default_value = prop["default"]
category = categories.get(key)
if category == "path":
path_params[key] = default_value
elif category == "query":
query_params[key] = default_value
elif category == "body":
body_params[key] = default_value
elif category == "header" and not api_key:
api_key = default_value
# Fall back to the environment-configured API key when the tool call
# doesn't carry an explicit header value.
if not api_key:
api_key = get_api_key() or None
if api_key:
mcp_logger.info("工具调用未带 API Key已回退到环境变量 datasourceApiKey")
client = _get_api_client()
result = client.request(
method=tool_def["method"],
path_template=tool_def["path"],
path_params=path_params,
query_params=query_params,
body=body_params if body_params else None,
file_path=file_path,
api_key=api_key,
)
return _text_response(result)
except Exception as e:
error_msg = f"工具调用失败: {name}, 错误: {e}"
mcp_logger.error(error_msg, exc_info=True)
return _text_response({"error": error_msg})
async def async_main():
"""Async entry for the MCP server."""
try:
mcp_logger.info("=" * 60)
mcp_logger.info("正在启动 MCP 服务: lzwcai-mcp-agile-db-third")
mcp_logger.info("版本: 0.1.0")
mcp_logger.info("=" * 60)
env = get_env_config()
mcp_logger.info(f"环境配置 - Backend Base URL: {env.get('backend_base_url')}")
mcp_logger.info(f"环境配置 - API Key: {'已设置' if env.get('api_key') else '未设置'}")
mcp_logger.info("=" * 60)
from mcp.server.stdio import stdio_server
async with stdio_server() as (read_stream, write_stream):
mcp_logger.info("MCP 服务已启动,等待客户端连接...")
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="lzwcai-mcp-agile-db-third",
server_version="0.1.2",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
mcp_logger.info("MCP 服务已关闭")
except Exception as e:
mcp_logger.error(f"MCP 服务运行失败: {e}", exc_info=True)
raise
def main():
"""Console entrypoint."""
try:
logger_config.setup_logging(
app_name="lzwcai_mcp_agile_db_third",
log_level=logging.INFO,
console_output=False,
)
mcp_logger.info("开始运行 MCP Agile DB Third 服务")
asyncio.run(async_main())
except KeyboardInterrupt:
mcp_logger.info("收到中断信号,正在关闭服务...")
except Exception as e:
mcp_logger.error(f"程序运行失败: {e}", exc_info=True)
raise
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,137 @@
"""Docker-based PostgreSQL mock for local self-testing.
Provides a throwaway Postgres container that the backend can connect to during
external-connection tests. The container is bound to 0.0.0.0:5432 on the Docker
host, so the host IP must be reachable from the backend server.
"""
import socket
import subprocess
import sys
import time
from typing import Optional
DEFAULT_IMAGE = "postgres:16-alpine"
DEFAULT_CONTAINER = "lzwcai_mcp_agile_db_third_mock_pg"
DEFAULT_PORT = 5432
DEFAULT_DATABASE = "postgres"
DEFAULT_USERNAME = "postgres"
DEFAULT_PASSWORD = "postgres"
def _run(args: list, check: bool = False, capture: bool = True) -> subprocess.CompletedProcess:
"""Run a shell command and return the result."""
return subprocess.run(
args,
check=check,
capture_output=capture,
text=True,
shell=False,
)
def host_ip(prefer_prefixes: Optional[list] = None) -> str:
"""Return a non-loopback IPv4 address of this machine.
Prefers addresses starting with the given prefixes so users can steer the
result toward the network segment that the backend can reach.
"""
prefer_prefixes = prefer_prefixes or ["192.168.", "10.", "172."]
hostname = socket.gethostname()
_, _, ips = socket.gethostbyname_ex(hostname)
# Prefer addresses matching a requested prefix, then any non-loopback.
for prefix in prefer_prefixes:
for ip in ips:
if ip.startswith(prefix) and not ip.startswith("127."):
return ip
for ip in ips:
if not ip.startswith("127."):
return ip
raise RuntimeError("Could not find a non-loopback IPv4 address on this host")
def is_running(container: str = DEFAULT_CONTAINER) -> bool:
"""Check whether the mock container is currently running."""
result = _run(["docker", "ps", "--filter", f"name={container}", "--format", "{{.Names}}"])
return container in result.stdout
def start(
container: str = DEFAULT_CONTAINER,
port: int = DEFAULT_PORT,
username: str = DEFAULT_USERNAME,
password: str = DEFAULT_PASSWORD,
database: str = DEFAULT_DATABASE,
image: str = DEFAULT_IMAGE,
) -> str:
"""Start the Postgres mock container if not already running.
Returns the host IP that should be supplied to backend connection tests.
"""
if is_running(container):
print(f"Mock DB container '{container}' is already running.")
return host_ip()
# Remove any stale container with the same name.
_run(["docker", "rm", "-f", container], check=False)
print(f"Starting mock Postgres container '{container}' (image={image})...")
_run(
[
"docker", "run", "-d",
"--name", container,
"-p", f"{port}:{port}",
"-e", f"POSTGRES_USER={username}",
"-e", f"POSTGRES_PASSWORD={password}",
"-e", f"POSTGRES_DB={database}",
image,
],
check=True,
)
print("Waiting for Postgres to become ready...")
deadline = time.time() + 30
while time.time() < deadline:
result = _run(
["docker", "exec", container, "pg_isready", "-U", username],
check=False,
)
if result.returncode == 0:
break
time.sleep(0.5)
else:
stop(container)
raise RuntimeError("Postgres mock failed to become ready within 30s")
ip = host_ip()
print(f"Mock Postgres is ready at {ip}:{port} (user={username}, password={password}, db={database})")
return ip
def stop(container: str = DEFAULT_CONTAINER) -> None:
"""Stop and remove the mock Postgres container."""
print(f"Stopping mock DB container '{container}'...")
_run(["docker", "stop", "-t", "5", container], check=False)
_run(["docker", "rm", "-f", container], check=False)
def main():
if len(sys.argv) < 2:
print("Usage: python -m lzwcai_mcp_agile_db_third.mock_db [start|stop|ip]")
sys.exit(1)
cmd = sys.argv[1].lower()
if cmd == "start":
start()
elif cmd == "stop":
stop()
elif cmd in ("ip", "host"):
print(host_ip())
elif cmd == "status":
print("running" if is_running() else "stopped")
else:
print(f"Unknown command: {cmd}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,41 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "lzwcai-mcp-agile-db-third"
version = "0.1.3"
description = "MCP server for Agile DB third-party datasource APIs"
readme = "README.md"
requires-python = ">=3.12"
license = {text = "MIT"}
authors = [
{name = "lzwcai", email = "your-email@example.com"},
]
keywords = ["mcp", "datasource", "database", "agile-db"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"httpx>=0.28.1",
"mcp[cli]>=1.10.1",
]
[project.scripts]
lzwcai-mcp-agile-db-third = "lzwcai_mcp_agile_db_third.main:main"
[tool.hatch.build.targets.wheel]
packages = ["lzwcai_mcp_agile_db_third"]
[tool.hatch.build.targets.wheel.force-include]
[tool.hatch.build]
exclude = [
"lzwcai_mcp_agile_db_third/logs/**",
"**/.__pycache__/**",
"lzwcai_mcp_agile_db_third/.gitignore",
".venv/**",
]

View File

@@ -0,0 +1,870 @@
"""MCP tool definitions for datasource API."""
from typing import Any, Dict, List
def _tool(
name: str,
description: str,
method: str,
path: str,
properties: Dict[str, Any],
required: List[str],
param_categories: Dict[str, str],
) -> Dict[str, Any]:
"""Build a tool definition dictionary."""
return {
"name": name,
"description": description,
"method": method,
"path": path,
"inputSchema": {
"type": "object",
"properties": properties,
"required": required,
},
"paramCategories": param_categories,
}
# 公共参数 schema 片段
_API_KEY_PROP = {
"apiKey": {
"type": "string",
"description": "可选的 X-Datasource-API-Key 权限密钥",
}
}
_TARGET_PROP = {
"target": {
"type": "string",
"description": "目标环境prod/test默认 prod",
"default": "prod",
}
}
_PAGE_PROPS = {
"pageNum": {"type": "integer", "description": "页码默认1", "default": 1},
"pageSize": {"type": "integer", "description": "每页数量默认10", "default": 10},
}
# =============================================================================
# 1. 数据源配置管理
# =============================================================================
LIST_DATASOURCE_CONFIGS = _tool(
name="list_datasource_configs",
description="查询数据源配置列表,支持分页和多条件筛选",
method="GET",
path="/datasource/config/list",
properties={
"datasourceName": {"type": "string", "description": "数据源名称(模糊查询)"},
"datasourceId": {"type": "integer", "description": "连接ID"},
"datasourceType": {"type": "string", "description": "数据库类型"},
"status": {"type": "integer", "description": "状态"},
"showTable": {"type": "boolean", "description": "是否显示表数量"},
**_PAGE_PROPS,
},
required=[],
param_categories={
"datasourceName": "query",
"datasourceId": "query",
"datasourceType": "query",
"status": "query",
"showTable": "query",
"pageNum": "query",
"pageSize": "query",
},
)
GET_DATASOURCE_CONFIG = _tool(
name="get_datasource_config",
description="获取指定数据源配置的详细信息",
method="GET",
path="/datasource/config/{id}",
properties={
"id": {"type": "integer", "description": "数据源配置ID"},
},
required=["id"],
param_categories={"id": "path"},
)
BATCH_CREATE_DATASOURCE_CONFIGS = _tool(
name="batch_create_datasource_configs",
description="批量创建数据源配置并同步表结构",
method="POST",
path="/datasource/config",
properties={
"connectionId": {"type": "integer", "description": "连接实例ID"},
"datasourceNamePrefix": {"type": "string", "description": "数据源名称前缀"},
"enterpriseId": {"type": "integer", "description": "企业ID"},
"status": {"type": "integer", "description": "状态"},
"remark": {"type": "string", "description": "备注信息"},
"syncTables": {"type": "boolean", "description": "是否同步表结构"},
"databases": {
"type": "array",
"description": "数据库与表列表",
"items": {
"type": "object",
"properties": {
"databaseName": {"type": "string"},
"tableNames": {"type": "array", "items": {"type": "string"}},
},
},
},
},
required=["connectionId", "databases"],
param_categories={
"connectionId": "body",
"datasourceNamePrefix": "body",
"enterpriseId": "body",
"status": "body",
"remark": "body",
"syncTables": "body",
"databases": "body",
},
)
REPLACE_DATASOURCE_CONFIGS = _tool(
name="replace_datasource_configs",
description="全量替换指定连接下的数据源配置(未传入的配置将被删除),请谨慎操作",
method="PUT",
path="/datasource/config",
properties={
"connectionId": {"type": "integer", "description": "连接实例ID"},
"datasourceNamePrefix": {"type": "string", "description": "数据源名称前缀"},
"enterpriseId": {"type": "integer", "description": "企业ID"},
"status": {"type": "integer", "description": "状态"},
"remark": {"type": "string", "description": "备注信息"},
"syncTables": {"type": "boolean", "description": "是否同步表结构"},
"databases": {
"type": "array",
"description": "数据库与表列表",
"items": {
"type": "object",
"properties": {
"databaseName": {"type": "string"},
"tableNames": {"type": "array", "items": {"type": "string"}},
},
},
},
},
required=["connectionId", "databases"],
param_categories={
"connectionId": "body",
"datasourceNamePrefix": "body",
"enterpriseId": "body",
"status": "body",
"remark": "body",
"syncTables": "body",
"databases": "body",
},
)
BATCH_UPDATE_DATASOURCE_CONFIGS = _tool(
name="batch_update_datasource_configs",
description="批量修改数据源配置并重新同步表结构",
method="PUT",
path="/datasource/config/batch",
properties={
"status": {"type": "integer", "description": "状态"},
"remark": {"type": "string", "description": "批量更新备注"},
"syncTables": {"type": "boolean", "description": "是否同步表结构"},
"datasources": {
"type": "array",
"description": "要更新的数据源配置列表",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"datasourceName": {"type": "string"},
"tableNames": {"type": "array", "items": {"type": "string"}},
},
},
},
},
required=["datasources"],
param_categories={
"status": "body",
"remark": "body",
"syncTables": "body",
"datasources": "body",
},
)
DELETE_DATASOURCE_CONFIGS = _tool(
name="delete_datasource_configs",
description="批量删除数据源配置",
method="POST",
path="/datasource/config/deletes",
properties={
"ids": {
"type": "array",
"description": "要删除的数据源配置ID列表",
"items": {"type": "integer"},
},
},
required=["ids"],
param_categories={"ids": "body"},
)
TEST_CONNECTION_CONFIG = _tool(
name="test_connection_config",
description="测试数据库连接是否正常",
method="POST",
path="/datasource/config/testConnectionConfig",
properties={
"host": {"type": "string", "description": "主机地址"},
"port": {"type": "integer", "description": "端口"},
"username": {"type": "string", "description": "用户名"},
"password": {"type": "string", "description": "密码"},
"datasourceType": {"type": "string", "description": "数据库类型,例如 PostgreSQL默认 PostgreSQL", "default": "PostgreSQL"},
},
required=["host", "port", "username", "password"],
param_categories={
"host": "body",
"port": "body",
"username": "body",
"password": "body",
"datasourceType": "body",
},
)
CHANGE_DATASOURCE_STATUS = _tool(
name="change_datasource_status",
description="修改数据源配置的启用/禁用状态",
method="PUT",
path="/datasource/config/changeStatus",
properties={
"id": {"type": "integer", "description": "数据源配置ID"},
"status": {"type": "integer", "description": "状态0正常/1停用"},
},
required=["id", "status"],
param_categories={"id": "body", "status": "body"},
)
EXPORT_DATASOURCE_CONFIGS = _tool(
name="export_datasource_configs",
description="导出数据源配置列表为Excel文件",
method="POST",
path="/datasource/config/export",
properties={
"datasourceName": {"type": "string", "description": "数据源名称(模糊查询)"},
"datasourceId": {"type": "integer", "description": "连接ID"},
"datasourceType": {"type": "string", "description": "数据库类型"},
"status": {"type": "integer", "description": "状态"},
},
required=[],
param_categories={
"datasourceName": "query",
"datasourceId": "query",
"datasourceType": "query",
"status": "query",
},
)
# =============================================================================
# 2. 数据库连接实例管理
# =============================================================================
LIST_CONNECTIONS = _tool(
name="list_connections",
description="查询数据库连接实例列表,支持分页和筛选",
method="GET",
path="/datasource/connection/list",
properties={
"datasourceName": {"type": "string", "description": "数据源名称(模糊查询)"},
"status": {"type": "integer", "description": "状态"},
"testStatus": {"type": "integer", "description": "测试状态0未测试/1成功/2失败"},
"sourceType": {"type": "string", "description": "连接来源builtin/external"},
**_PAGE_PROPS,
},
required=[],
param_categories={
"datasourceName": "query",
"status": "query",
"testStatus": "query",
"sourceType": "query",
"pageNum": "query",
"pageSize": "query",
},
)
GET_CONNECTION = _tool(
name="get_connection",
description="获取指定连接实例的详细信息",
method="GET",
path="/datasource/connection/{id}",
properties={
"id": {"type": "integer", "description": "连接实例ID"},
},
required=["id"],
param_categories={"id": "path"},
)
CREATE_CONNECTION = _tool(
name="create_connection",
description="创建新的数据库连接实例",
method="POST",
path="/datasource/connection",
properties={
"datasourceName": {"type": "string", "description": "连接名称"},
"datasourceType": {"type": "string", "description": "数据库类型,例如 PostgreSQL默认 PostgreSQL", "default": "PostgreSQL"},
"connectionType": {"type": "string", "description": "连接方式,例如 user_password默认 user_password", "default": "user_password"},
"host": {"type": "string", "description": "主机地址"},
"port": {"type": "integer", "description": "端口"},
"username": {"type": "string", "description": "用户名"},
"password": {"type": "string", "description": "密码"},
"status": {"type": "integer", "description": "状态"},
"remark": {"type": "string", "description": "备注信息"},
},
required=["datasourceName", "host", "port", "username", "password"],
param_categories={
"datasourceName": "body",
"datasourceType": "body",
"connectionType": "body",
"host": "body",
"port": "body",
"username": "body",
"password": "body",
"status": "body",
"remark": "body",
},
)
UPDATE_CONNECTION = _tool(
name="update_connection",
description="修改数据库连接实例信息",
method="PUT",
path="/datasource/connection",
properties={
"id": {"type": "integer", "description": "连接实例ID"},
"datasourceName": {"type": "string", "description": "连接名称"},
"host": {"type": "string", "description": "主机地址"},
"port": {"type": "integer", "description": "端口"},
"username": {"type": "string", "description": "用户名"},
"password": {"type": "string", "description": "密码"},
"remark": {"type": "string", "description": "更新备注"},
},
required=["id"],
param_categories={
"id": "body",
"datasourceName": "body",
"host": "body",
"port": "body",
"username": "body",
"password": "body",
"remark": "body",
},
)
DELETE_CONNECTION = _tool(
name="delete_connection",
description="删除指定的连接实例",
method="DELETE",
path="/datasource/connection/{id}",
properties={
"id": {"type": "integer", "description": "连接实例ID"},
},
required=["id"],
param_categories={"id": "path"},
)
TEST_CONNECTION = _tool(
name="test_connection",
description="测试数据库连接是否可用",
method="POST",
path="/datasource/connection/test",
properties={
"id": {"type": "integer", "description": "连接实例ID"},
"host": {"type": "string", "description": "主机地址"},
"port": {"type": "integer", "description": "端口"},
"username": {"type": "string", "description": "用户名"},
"password": {"type": "string", "description": "密码"},
"datasourceType": {"type": "string", "description": "数据库类型,例如 PostgreSQL默认 PostgreSQL", "default": "PostgreSQL"},
},
required=["host", "port", "username", "password"],
param_categories={
"id": "body",
"host": "body",
"port": "body",
"username": "body",
"password": "body",
"datasourceType": "body",
},
)
CHANGE_CONNECTION_STATUS = _tool(
name="change_connection_status",
description="修改连接实例的启用/禁用状态",
method="PUT",
path="/datasource/connection/changeStatus",
properties={
"id": {"type": "integer", "description": "连接实例ID"},
"status": {"type": "integer", "description": "状态0正常/1停用"},
},
required=["id", "status"],
param_categories={"id": "body", "status": "body"},
)
REALTIME_STRUCTURE = _tool(
name="realtime_structure",
description="实时查询连接下的所有数据库和表结构(直接连接数据库服务器查询)",
method="GET",
path="/datasource/connection/realtime/structure/{id}",
properties={
"id": {"type": "integer", "description": "连接实例ID"},
},
required=["id"],
param_categories={"id": "path"},
)
REALTIME_DATABASES = _tool(
name="realtime_databases",
description="实时查询连接下的所有数据库名称",
method="GET",
path="/datasource/connection/realtime/databases/{id}",
properties={
"id": {"type": "integer", "description": "连接实例ID"},
},
required=["id"],
param_categories={"id": "path"},
)
REALTIME_TABLES = _tool(
name="realtime_tables",
description="实时查询指定数据库下的所有表",
method="GET",
path="/datasource/connection/realtime/tables/{id}",
properties={
"id": {"type": "integer", "description": "连接实例ID"},
"databaseName": {"type": "string", "description": "数据库名称"},
},
required=["id", "databaseName"],
param_categories={"id": "path", "databaseName": "query"},
)
CREATE_BUILTIN_POSTGRESQL = _tool(
name="create_builtin_postgresql",
description="创建内置 PostgreSQL 数据库连接(使用配置文件中的连接信息)",
method="POST",
path="/datasource/connection/create_builtin_postgresql",
properties={
"datasourceName": {"type": "string", "description": "连接名称"},
"remark": {"type": "string", "description": "备注"},
},
required=["datasourceName"],
param_categories={"datasourceName": "body", "remark": "body"},
)
UPDATE_BUILTIN_DATABASE = _tool(
name="update_builtin_database",
description="修改内置 PostgreSQL 连接信息",
method="PUT",
path="/datasource/connection/update_builtin_database",
properties={
"connectionId": {"type": "integer", "description": "连接实例ID"},
"datasourceName": {"type": "string", "description": "新名称"},
"remark": {"type": "string", "description": "更新备注"},
},
required=["connectionId"],
param_categories={"connectionId": "body", "datasourceName": "body", "remark": "body"},
)
EXECUTE_SQL = _tool(
name="execute_sql",
description="在指定数据源上执行 SQL 语句,支持参数化查询和环境切换",
method="POST",
path="/datasource/connection/{datasourceId}/execute_sql",
properties={
"datasourceId": {"type": "integer", "description": "数据源配置ID"},
"target": {
"type": "string",
"description": "目标环境prod/test默认 prod",
"default": "prod",
},
"sql": {"type": "string", "description": "SQL 语句,使用 ? 占位符"},
"params": {
"type": "array",
"description": "SQL 参数列表",
"items": {},
},
"databaseName": {"type": "string", "description": "数据库名称"},
**_API_KEY_PROP,
},
required=["datasourceId", "sql"],
param_categories={
"datasourceId": "path",
"target": "query",
"sql": "body",
"params": "body",
"databaseName": "body",
"apiKey": "header",
},
)
CREATE_DATABASE = _tool(
name="create_database",
description="在指定连接上创建新数据库(目前仅支持 PostgreSQL",
method="POST",
path="/datasource/connection/{connectionId}/create_database",
properties={
"connectionId": {"type": "integer", "description": "连接实例ID"},
"databaseName": {"type": "string", "description": "数据库名称"},
"encoding": {"type": "string", "description": "字符编码,例如 UTF8"},
"owner": {"type": "string", "description": "所有者"},
**_API_KEY_PROP,
},
required=["connectionId", "databaseName"],
param_categories={
"connectionId": "path",
"databaseName": "body",
"encoding": "body",
"owner": "body",
"apiKey": "header",
},
)
CREATE_TABLE = _tool(
name="create_table",
description="在指定数据库中创建新表",
method="POST",
path="/datasource/connection/{connectionId}/create_table",
properties={
"connectionId": {"type": "integer", "description": "连接实例ID"},
"databaseName": {"type": "string", "description": "数据库名称"},
"tableName": {"type": "string", "description": "表名"},
"tableComment": {"type": "string", "description": "表注释"},
"columns": {
"type": "array",
"description": "列定义列表",
"items": {
"type": "object",
"properties": {
"columnName": {"type": "string"},
"columnType": {"type": "string"},
"columnLength": {"type": "integer"},
"isPrimaryKey": {"type": "boolean"},
"isNullable": {"type": "boolean"},
"columnComment": {"type": "string"},
},
},
},
**_API_KEY_PROP,
},
required=["connectionId", "databaseName", "tableName", "columns"],
param_categories={
"connectionId": "path",
"databaseName": "body",
"tableName": "body",
"tableComment": "body",
"columns": "body",
"apiKey": "header",
},
)
CREATE_DATABASE_TABLE = _tool(
name="create_database_table",
description="同时创建数据库和表(一次性操作)",
method="POST",
path="/datasource/connection/{connectionId}/create_database_table",
properties={
"connectionId": {"type": "integer", "description": "连接实例ID"},
"databaseName": {"type": "string", "description": "数据库名称"},
"encoding": {"type": "string", "description": "字符编码"},
"owner": {"type": "string", "description": "所有者"},
"tables": {
"type": "array",
"description": "要创建的表列表",
"items": {
"type": "object",
"properties": {
"tableName": {"type": "string"},
"tableComment": {"type": "string"},
"columns": {"type": "array"},
},
},
},
**_API_KEY_PROP,
},
required=["connectionId", "databaseName", "tables"],
param_categories={
"connectionId": "path",
"databaseName": "body",
"encoding": "body",
"owner": "body",
"tables": "body",
"apiKey": "header",
},
)
ALTER_DATABASE = _tool(
name="alter_database",
description="修改数据库属性(重命名、更改所有者、更改编码)",
method="PUT",
path="/datasource/connection/{connectionId}/alter_database",
properties={
"connectionId": {"type": "integer", "description": "连接实例ID"},
"databaseName": {"type": "string", "description": "原数据库名称"},
"newName": {"type": "string", "description": "新数据库名称"},
"newOwner": {"type": "string", "description": "新所有者"},
"newEncoding": {"type": "string", "description": "新字符编码"},
**_API_KEY_PROP,
},
required=["connectionId", "databaseName"],
param_categories={
"connectionId": "path",
"databaseName": "body",
"newName": "body",
"newOwner": "body",
"newEncoding": "body",
"apiKey": "header",
},
)
ALTER_TABLE = _tool(
name="alter_table",
description="修改表结构(添加列、删除列、重命名列、修改列类型等)",
method="PUT",
path="/datasource/connection/{connectionId}/alter_table",
properties={
"connectionId": {"type": "integer", "description": "连接实例ID"},
"databaseName": {"type": "string", "description": "数据库名称"},
"tableName": {"type": "string", "description": "表名"},
"tableComment": {"type": "string", "description": "表注释"},
"operations": {
"type": "array",
"description": "操作列表",
"items": {
"type": "object",
"properties": {
"operation": {"type": "string"},
"column": {"type": "object"},
},
},
},
**_API_KEY_PROP,
},
required=["connectionId", "databaseName", "tableName", "operations"],
param_categories={
"connectionId": "path",
"databaseName": "body",
"tableName": "body",
"tableComment": "body",
"operations": "body",
"apiKey": "header",
},
)
GENERATE_TABLE = _tool(
name="generate_table",
description="使用 AI 根据需求描述生成表结构(异步任务)",
method="POST",
path="/datasource/connection/generate_table",
properties={
"requirement": {"type": "string", "description": "需求描述"},
"databaseId": {"type": "integer", "description": "关联的数据库ID"},
},
required=["requirement"],
param_categories={"requirement": "body", "databaseId": "body"},
)
IMPORT_DOCUMENT_PREVIEW = _tool(
name="import_document_preview",
description="上传 Excel/CSV 文件AI 识别表结构并预览前 10 条数据",
method="POST",
path="/datasource/connection/{connectionId}/import_document/preview",
properties={
"connectionId": {"type": "integer", "description": "连接实例ID"},
"target": {
"type": "string",
"description": "目标环境prod/test默认 prod",
"default": "prod",
},
"filePath": {"type": "string", "description": "本地 Excel/CSV 文件路径"},
},
required=["connectionId", "filePath"],
param_categories={
"connectionId": "path",
"target": "query",
"filePath": "file",
},
)
IMPORT_DOCUMENT_CONFIRM = _tool(
name="import_document_confirm",
description="确认导入,创建表并插入数据",
method="POST",
path="/datasource/connection/{connectionId}/import_document/confirm",
properties={
"connectionId": {"type": "integer", "description": "连接实例ID"},
"target": {
"type": "string",
"description": "目标环境prod/test默认 prod",
"default": "prod",
},
"tableStructure": {
"type": "object",
"description": "表结构定义",
},
"allData": {
"type": "array",
"description": "完整导入数据",
},
**_API_KEY_PROP,
},
required=["connectionId", "tableStructure", "allData"],
param_categories={
"connectionId": "path",
"target": "query",
"tableStructure": "body",
"allData": "body",
"apiKey": "header",
},
)
BUILTIN_TABLE_DATA = _tool(
name="builtin_table_data",
description="根据表ID查询表结构和数据分页",
method="GET",
path="/datasource/connection/builtin/table/{tableId}",
properties={
"tableId": {"type": "integer", "description": "表ID"},
"pageNum": {"type": "integer", "description": "页码默认1"},
"pageSize": {"type": "integer", "description": "每页数量默认100"},
"target": {
"type": "string",
"description": "目标环境prod/test默认 prod",
"default": "prod",
},
},
required=["tableId"],
param_categories={
"tableId": "path",
"pageNum": "query",
"pageSize": "query",
"target": "query",
},
)
BUILTIN_TABLE_INSERT = _tool(
name="builtin_table_insert",
description="向指定表插入一条数据",
method="POST",
path="/datasource/connection/builtin/table/{tableId}/rows",
properties={
"tableId": {"type": "integer", "description": "表ID"},
"target": {
"type": "string",
"description": "目标环境prod/test默认 prod",
"default": "prod",
},
"data": {"type": "object", "description": "要插入的数据"},
},
required=["tableId", "data"],
param_categories={
"tableId": "path",
"target": "query",
"data": "body",
},
)
BUILTIN_TABLE_UPDATE = _tool(
name="builtin_table_update",
description="根据主键更新表数据",
method="PUT",
path="/datasource/connection/builtin/table/{tableId}/rows",
properties={
"tableId": {"type": "integer", "description": "表ID"},
"target": {
"type": "string",
"description": "目标环境prod/test默认 prod",
"default": "prod",
},
"data": {"type": "object", "description": "要更新的数据"},
"primaryKey": {"type": "object", "description": "主键条件"},
},
required=["tableId", "data", "primaryKey"],
param_categories={
"tableId": "path",
"target": "query",
"data": "body",
"primaryKey": "body",
},
)
BUILTIN_TABLE_DELETE = _tool(
name="builtin_table_delete",
description="根据主键批量删除表数据",
method="DELETE",
path="/datasource/connection/builtin/table/{tableId}/rows",
properties={
"tableId": {"type": "integer", "description": "表ID"},
"target": {
"type": "string",
"description": "目标环境prod/test默认 prod",
"default": "prod",
},
"primaryKeys": {
"type": "array",
"description": "主键条件列表",
"items": {"type": "object"},
},
},
required=["tableId", "primaryKeys"],
param_categories={
"tableId": "path",
"target": "query",
"primaryKeys": "body",
},
)
ALL_TOOLS = [
LIST_DATASOURCE_CONFIGS,
GET_DATASOURCE_CONFIG,
BATCH_CREATE_DATASOURCE_CONFIGS,
REPLACE_DATASOURCE_CONFIGS,
BATCH_UPDATE_DATASOURCE_CONFIGS,
DELETE_DATASOURCE_CONFIGS,
TEST_CONNECTION_CONFIG,
CHANGE_DATASOURCE_STATUS,
EXPORT_DATASOURCE_CONFIGS,
LIST_CONNECTIONS,
GET_CONNECTION,
CREATE_CONNECTION,
UPDATE_CONNECTION,
DELETE_CONNECTION,
TEST_CONNECTION,
CHANGE_CONNECTION_STATUS,
REALTIME_STRUCTURE,
REALTIME_DATABASES,
REALTIME_TABLES,
CREATE_BUILTIN_POSTGRESQL,
UPDATE_BUILTIN_DATABASE,
EXECUTE_SQL,
CREATE_DATABASE,
CREATE_TABLE,
CREATE_DATABASE_TABLE,
ALTER_DATABASE,
ALTER_TABLE,
GENERATE_TABLE,
IMPORT_DOCUMENT_PREVIEW,
IMPORT_DOCUMENT_CONFIRM,
BUILTIN_TABLE_DATA,
BUILTIN_TABLE_INSERT,
BUILTIN_TABLE_UPDATE,
BUILTIN_TABLE_DELETE,
]
TOOL_MAP = {tool["name"]: tool for tool in ALL_TOOLS}
def list_tools() -> List[Dict[str, Any]]:
"""Return all tool definitions."""
return ALL_TOOLS
def get_tool(name: str) -> Dict[str, Any]:
"""Return a tool definition by name."""
return TOOL_MAP.get(name)

View File

@@ -0,0 +1,14 @@
"""Utils package for lzwcai_mcp_agile_db_third."""
from .env_config import get_backend_base_url, get_api_key, get_env_config
from .api_client import DataSourceAPIClient, api_request
from .logger_config import logger_config
__all__ = [
"get_backend_base_url",
"get_api_key",
"get_env_config",
"DataSourceAPIClient",
"api_request",
"logger_config",
]

View File

@@ -0,0 +1,223 @@
"""Backend API client for datasource APIs."""
import base64
import logging
import mimetypes
import os
from typing import Any, Dict, Optional
from urllib.parse import urlencode
import httpx
try:
from .env_config import get_backend_base_url
except ImportError:
from env_config import get_backend_base_url
logger = logging.getLogger(__name__)
DEFAULT_TOKEN = (
""
)
_SENSITIVE_FIELDS = {"password", "apiKey", "token", "secret"}
def _mask_secret(value: Optional[str]) -> str:
"""Mask a secret for logging, keeping only a few leading/trailing chars."""
if not value:
return "<空>"
if len(value) <= 8:
return "***"
return f"{value[:4]}...{value[-4:]} (len={len(value)})"
class DataSourceAPIClient:
"""HTTP client for backend datasource APIs."""
def __init__(
self,
base_url: Optional[str] = None,
token: Optional[str] = None,
):
self.base_url = (base_url or get_backend_base_url()).rstrip("/")
# Prefer explicit token, then API_KEY env var, then built-in default.
resolved = token or os.environ.get("API_KEY") or DEFAULT_TOKEN
self.token = resolved.removeprefix("Bearer ").strip()
self.client = httpx.Client(timeout=120.0)
def close(self) -> None:
"""Close the underlying HTTP client."""
self.client.close()
def _get_headers(self, api_key: Optional[str] = None) -> Dict[str, str]:
headers = {
"Authorization": f"Bearer {self.token}",
"Accept": "application/json",
}
if api_key:
headers["X-Datasource-API-Key"] = api_key
return headers
@staticmethod
def _sanitize_value(value: Any) -> Any:
"""Redact sensitive nested values for logging."""
if isinstance(value, dict):
return {
k: "***" if k in _SENSITIVE_FIELDS else DataSourceAPIClient._sanitize_value(v)
for k, v in value.items()
}
if isinstance(value, list):
return [DataSourceAPIClient._sanitize_value(item) for item in value]
return value
@staticmethod
def _file_tuple(file_path: str) -> tuple:
"""Build a multipart file tuple with filename and content type."""
filename = os.path.basename(file_path)
content_type, _ = mimetypes.guess_type(filename)
if content_type is None:
content_type = "application/octet-stream"
return (filename, open(file_path, "rb"), content_type)
def request(
self,
method: str,
path_template: str,
path_params: Optional[Dict[str, Any]] = None,
query_params: Optional[Dict[str, Any]] = None,
body: Optional[Dict[str, Any]] = None,
file_path: Optional[str] = None,
api_key: Optional[str] = None,
) -> Dict[str, Any]:
"""Send a request and return JSON response."""
path_params = path_params or {}
query_params = query_params or {}
try:
url = self._build_url(path_template, path_params, query_params)
headers = self._get_headers(api_key)
safe_body = self._sanitize_value(body) if body else body
logger.info(f"调用后端 API: {method} {url}")
logger.info(
f"请求密钥 - X-Datasource-API-Key: {_mask_secret(api_key)}, "
f"Authorization token: {_mask_secret(self.token)}"
)
logger.debug(
f"path_params={path_params}, query_params={query_params}, "
f"body={safe_body}, file_path={file_path}"
)
method = method.upper()
if method == "GET":
response = self.client.get(url, headers=headers)
elif method == "DELETE":
if body is not None:
headers["Content-Type"] = "application/json"
response = self.client.request(method, url, headers=headers, json=body)
else:
response = self.client.delete(url, headers=headers)
elif file_path:
# Multipart upload
headers.pop("Content-Type", None)
file_tuple = self._file_tuple(file_path)
files = {"file": file_tuple}
try:
response = self.client.request(
method, url, headers=headers, data=body, files=files
)
finally:
file_tuple[1].close()
elif body is not None:
headers["Content-Type"] = "application/json"
response = self.client.request(method, url, headers=headers, json=body)
else:
response = self.client.request(method, url, headers=headers)
response.raise_for_status()
content_type = response.headers.get("content-type", "")
if "application/json" in content_type:
data = response.json()
else:
# Non-JSON response (e.g., Excel export), encode as base64
data = {
"success": True,
"contentType": content_type,
"fileBase64": base64.b64encode(response.content).decode("ascii"),
"message": "后端返回非 JSON 内容,已使用 base64 编码",
}
logger.info(f"后端 API 调用成功: {method} {url}")
logger.debug(f"响应: {data}")
return data
except httpx.TimeoutException as e:
error_msg = f"API 请求超时: {method} {path_template}"
logger.error(error_msg)
raise Exception(error_msg) from e
except httpx.HTTPStatusError as e:
error_msg = f"API 请求失败 (HTTP {e.response.status_code}): {e.response.text}"
logger.error(error_msg)
raise Exception(error_msg) from e
except httpx.RequestError as e:
error_msg = f"API 请求异常: {method} {path_template}, 错误: {e}"
logger.error(error_msg)
raise Exception(error_msg) from e
except Exception as e:
error_msg = f"处理 API 响应时出错: {e}"
logger.error(error_msg, exc_info=True)
raise Exception(error_msg) from e
def _build_url(
self,
path_template: str,
path_params: Dict[str, Any],
query_params: Dict[str, Any],
) -> str:
path = path_template
for key, value in path_params.items():
path = path.replace(f"{{{key}}}", str(value))
url = f"{self.base_url}{path}"
if query_params:
filtered = {k: v for k, v in query_params.items() if v is not None}
if filtered:
url = f"{url}?{urlencode(filtered)}"
return url
def api_request(
method: str,
path_template: str,
path_params: Optional[Dict[str, Any]] = None,
query_params: Optional[Dict[str, Any]] = None,
body: Optional[Dict[str, Any]] = None,
file_path: Optional[str] = None,
api_key: Optional[str] = None,
base_url: Optional[str] = None,
token: Optional[str] = None,
) -> Dict[str, Any]:
"""Convenience wrapper for one-off API requests."""
client = DataSourceAPIClient(base_url=base_url, token=token)
try:
return client.request(
method=method,
path_template=path_template,
path_params=path_params,
query_params=query_params,
body=body,
file_path=file_path,
api_key=api_key,
)
finally:
client.close()
# Module-level default client
_default_client = DataSourceAPIClient()
def get_default_client() -> DataSourceAPIClient:
return _default_client

View File

@@ -0,0 +1,31 @@
"""环境变量配置模块"""
import os
def get_backend_base_url(default: str = "http://lzwcai-demp-corp-manager:8086") -> str:
"""
获取后端 API 基础 URL。
Environment Variables:
backendBaseUrl: 后端 API 基础 URL
"""
return os.environ.get("backendBaseUrl", default)
def get_api_key(default: str = "") -> str:
"""
获取默认 API 密钥X-Datasource-API-Key
Environment Variables:
datasourceApiKey: 默认 API 密钥
"""
return os.environ.get("datasourceApiKey", default)
def get_env_config() -> dict:
"""获取所有环境配置。"""
return {
"backend_base_url": get_backend_base_url(),
"api_key": get_api_key(),
}

View File

@@ -0,0 +1,85 @@
"""统一日志配置模块"""
import logging
import os
import sys
from pathlib import Path
class LoggerConfig:
"""日志配置管理类"""
def __init__(self, logs_dir: str = None):
if logs_dir:
self.logs_dir = Path(logs_dir)
else:
project_root = Path(__file__).parent.parent
self.logs_dir = project_root / "logs"
self.logs_dir.mkdir(parents=True, exist_ok=True)
self.log_format = "%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s"
self.date_format = "%Y-%m-%d %H:%M:%S"
self._initialized = False
def setup_logging(
self,
app_name: str = "lzwcai_mcp_agile_db_third",
log_level: int = logging.INFO,
console_output: bool = False,
) -> logging.Logger:
"""设置系统日志配置。"""
if self._initialized:
return logging.getLogger()
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
formatter = logging.Formatter(self.log_format, self.date_format)
# 主日志文件
main_log_file = self.logs_dir / f"{app_name}.log"
file_handler = logging.FileHandler(main_log_file, encoding="utf-8")
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# 错误日志文件
error_log_file = self.logs_dir / f"{app_name}_error.log"
error_handler = logging.FileHandler(error_log_file, encoding="utf-8")
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(formatter)
root_logger.addHandler(error_handler)
# MCP 使用 stdio 时,控制台日志必须输出到 stderr
if console_output:
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(log_level)
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
self._initialized = True
root_logger.info(f"日志系统初始化完成 - 日志目录: {self.logs_dir}")
return root_logger
def setup_mcp_logging(self) -> logging.Logger:
"""设置 MCP 专用日志。"""
return self.create_component_logger("mcp_services", "mcp_services.log", logging.DEBUG)
def create_component_logger(
self, component_name: str, log_file: str = None, log_level: int = None
) -> logging.Logger:
"""为特定组件创建独立日志器。"""
logger = logging.getLogger(component_name)
if log_file:
component_log_file = self.logs_dir / log_file
handler = logging.FileHandler(component_log_file, encoding="utf-8")
handler.setFormatter(logging.Formatter(self.log_format, self.date_format))
if log_level is not None:
handler.setLevel(log_level)
logger.addHandler(handler)
return logger
logger_config = LoggerConfig()

View File

@@ -0,0 +1,17 @@
"""
Repository-local launcher for lzwcai-mcp-sqlexecutor.
"""
import os
def main():
# Keep local developer defaults without overriding explicit environment settings.
os.environ.setdefault("backendBaseUrl", "http://192.168.2.236:8088")
os.environ.setdefault("datasourceApiKey", "yBOHyhCSpExAoEleimSfhbRzsF6SDiPYdGGwowXG-Sk")
from lzwcai_mcp_agile_db_third.main import main
main()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,41 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "lzwcai-mcp-agile-db-third"
version = "0.1.6"
description = "MCP server for Agile DB third-party datasource APIs"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [
{name = "lzwcai", email = "your-email@example.com"},
]
keywords = ["mcp", "datasource", "database", "agile-db"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"httpx>=0.28.1",
"mcp[cli]>=1.10.1",
]
[project.scripts]
lzwcai-mcp-agile-db-third = "lzwcai_mcp_agile_db_third.main:main"
[tool.hatch.build.targets.wheel]
packages = ["lzwcai_mcp_agile_db_third"]
[tool.hatch.build.targets.wheel.force-include]
[tool.hatch.build]
exclude = [
"lzwcai_mcp_agile_db_third/logs/**",
"**/.__pycache__/**",
"lzwcai_mcp_agile_db_third/.gitignore",
".venv/**",
]

View File

@@ -0,0 +1,418 @@
"""End-to-end self-test for all lzwcai_mcp_agile_db_third MCP tools.
Runs every tool through the real MCP handler (handle_call_tool) against the
backend configured below. Uses selftest_-prefixed throwaway resources and
cleans them up at the end. The destructive full-replace tool is skipped.
"""
import asyncio
import csv
import json
import os
import sys
import tempfile
import uuid
os.environ.setdefault("backendBaseUrl", "http://192.168.2.236:8088")
# Login token (Authorization Bearer) shared with the first-party AgileDB MCP server.
os.environ.setdefault(
"API_KEY",
"Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6"
"ImNlMDAwYjA4LWU0YTYtNGM2MS1hNzJiLWI3NTlmNmY1N2Q4NCJ9.jiNmGQZfL4-nSIFrLuaCt7mT"
"5zj0FOojAVkLeHwPOroI5jBxodrCe1PSwGO1OHq5Ztb0tLEVZw2FFVj0OlTceQ",
)
# Optional X-Datasource-API-Key for datasource-level permission checks (if enforced).
os.environ.setdefault("datasourceApiKey", "Mggkz34Yk8cbjUvCvQ-qeooNRg62WhSwwtxUUV6e0Pg")
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from lzwcai_mcp_agile_db_third.main import handle_call_tool # noqa: E402
from lzwcai_mcp_agile_db_third.mock_db import start as start_mock_db, stop as stop_mock_db # noqa: E402
from lzwcai_mcp_agile_db_third.tools import ALL_TOOLS # noqa: E402
RESULTS = [] # (name, status, detail)
TESTED = set()
async def call(name, args=None):
"""Invoke a tool through the MCP handler and return parsed JSON."""
TESTED.add(name)
try:
resp = await handle_call_tool(name, args or {})
return json.loads(resp[0].text)
except Exception as e: # noqa: BLE001
return {"_exception": repr(e)}
def summarize(data):
"""Extract a short (ok, detail) from a backend response."""
if not isinstance(data, dict):
return False, str(data)[:200]
if "_exception" in data:
return False, data["_exception"][:200]
if "error" in data:
return False, str(data["error"])[:200]
code = data.get("code")
msg = data.get("msg", "")
if code == 200:
return True, f"code=200 msg={msg}"
if code is not None:
return False, f"code={code} msg={msg}"
# list/paginated or already-unwrapped payloads
return True, json.dumps(data, ensure_ascii=False)[:160]
def record(name, data, mode="ok"):
"""Record a tool result.
mode="ok" -> PASS only when backend code==200
mode="roundtrip" -> PASS when the call round-trips (no exception and the
backend returned a structured response), regardless of
business code. Used for ops whose success depends on a
real reachable DB we can't guarantee in self-test.
"""
ok, detail = summarize(data)
if mode == "roundtrip":
exception = isinstance(data, dict) and "_exception" in data
passed = not exception
else:
passed = ok
status = "PASS" if passed else "FAIL"
RESULTS.append((name, status, detail))
print(f"[{status}] {name}: {detail}")
return data
def dig(data, *keys, default=None):
"""Safely walk nested dict keys."""
cur = data
for k in keys:
if not isinstance(cur, dict):
return default
cur = cur.get(k)
return cur if cur is not None else default
async def main():
PREFIX = f"selftest_{uuid.uuid4().hex[:8]}_"
state = {}
# ---- 1. read-only: connections & configs ----------------------------
print("\n=== 只读查询 ===")
conns = record("list_connections", await call("list_connections", {"pageSize": 5}))
# pick a builtin connection to exercise builtin/realtime tools
rows = conns.get("rows") or dig(conns, "data", "rows", default=[]) or []
builtin_id = None
any_conn_id = None
for r in rows:
cid = r.get("id")
if any_conn_id is None:
any_conn_id = cid
if r.get("sourceType") == "builtin" and builtin_id is None:
builtin_id = cid
state["builtin_id"] = builtin_id
state["any_conn_id"] = any_conn_id
print(f" -> builtin_id={builtin_id}, any_conn_id={any_conn_id}")
record("list_datasource_configs", await call("list_datasource_configs", {"pageSize": 5}))
if any_conn_id:
record("get_connection", await call("get_connection", {"id": any_conn_id}))
record("realtime_databases", await call("realtime_databases", {"id": any_conn_id}))
record("realtime_structure", await call("realtime_structure", {"id": any_conn_id}))
else:
for n in ("get_connection", "realtime_databases", "realtime_structure"):
RESULTS.append((n, "SKIP", "no connection available"))
# ---- 2. create a builtin PostgreSQL connection ----------------------
print("\n=== 创建内置连接 ===")
created = record(
"create_builtin_postgresql",
await call("create_builtin_postgresql", {
"datasourceName": PREFIX + "conn",
"remark": "self-test connection",
}),
)
conn_id = dig(created, "data", "id")
if conn_id is None and builtin_id:
conn_id = builtin_id # fall back to an existing builtin for downstream tools
state["conn_id"] = conn_id
print(f" -> conn_id={conn_id}")
if conn_id:
record("update_builtin_database", await call("update_builtin_database", {
"connectionId": conn_id,
"datasourceName": PREFIX + "conn_renamed",
"remark": "renamed by self-test",
}))
record("get_connection", await call("get_connection", {"id": conn_id}))
# ---- 3. external connection: test/create/update/status/delete -------
print("\n=== 外部连接测试(依赖真实可达的库,可能失败属正常)===")
# Try to spin up a local Docker Postgres mock. If Docker is unavailable or
# the backend cannot reach the host IP, fall back to 127.0.0.1 and keep the
# round-trip assertion (we still verify the tool round-trips).
mock_host = "127.0.0.1"
try:
mock_host = start_mock_db()
print(f" -> mock DB available at {mock_host}:5432")
except Exception as e: # noqa: BLE001
print(f" -> could not start mock DB, using 127.0.0.1:5432 ({e})")
# test_connection_config / test_connection rely on datasourceType default now
record(
"test_connection_config",
await call("test_connection_config", {
"host": mock_host, "port": 5432,
"username": "postgres", "password": "postgres",
}),
mode="roundtrip", # may still fail if backend cannot reach mock_host
)
# create_connection (external) — exercises datasourceType/connectionType defaults
ext = record(
"create_connection",
await call("create_connection", {
"datasourceName": PREFIX + "ext",
"host": mock_host, "port": 5432,
"username": "postgres", "password": "postgres",
"remark": "self-test external",
}),
mode="roundtrip",
)
ext_id = dig(ext, "data", "id")
state["ext_id"] = ext_id
print(f" -> ext_id={ext_id}")
if ext_id:
record("update_connection", await call("update_connection", {
"id": ext_id, "datasourceName": PREFIX + "ext_renamed",
"remark": "renamed",
}), mode="roundtrip")
record("test_connection", await call("test_connection", {
"id": ext_id, "host": mock_host, "port": 5432,
"username": "postgres", "password": "postgres",
}), mode="roundtrip")
record("change_connection_status", await call("change_connection_status", {
"id": ext_id, "status": 1,
}), mode="roundtrip")
else:
for n in ("update_connection", "test_connection", "change_connection_status"):
RESULTS.append((n, "SKIP", "no external connection id returned"))
# ---- 4. DDL on the builtin connection ------------------------------
print("\n=== DDL库/表 ===")
ddl_conn = conn_id or builtin_id
db_name = PREFIX + "db"
tbl_name = PREFIX + "users"
cols = [
{"columnName": "id", "columnType": "BIGINT", "isPrimaryKey": True,
"isNullable": False, "columnComment": "主键"},
{"columnName": "name", "columnType": "VARCHAR", "columnLength": 100,
"isNullable": False, "columnComment": "姓名"},
{"columnName": "age", "columnType": "INTEGER", "isNullable": True,
"columnComment": "年龄"},
]
if ddl_conn:
record("create_database", await call("create_database", {
"connectionId": ddl_conn, "databaseName": db_name,
"encoding": "UTF8",
}), mode="roundtrip")
record("create_table", await call("create_table", {
"connectionId": ddl_conn, "databaseName": db_name,
"tableName": tbl_name, "tableComment": "self-test 用户表",
"columns": cols,
}), mode="roundtrip")
record("realtime_tables", await call("realtime_tables", {
"id": ddl_conn, "databaseName": db_name,
}), mode="roundtrip")
record("create_database_table", await call("create_database_table", {
"connectionId": ddl_conn, "databaseName": PREFIX + "db2",
"encoding": "UTF8",
"tables": [{"tableName": PREFIX + "t2", "tableComment": "二号表",
"columns": cols}],
}), mode="roundtrip")
record("alter_table", await call("alter_table", {
"connectionId": ddl_conn, "databaseName": db_name,
"tableName": tbl_name,
"operations": [{"operation": "ADD_COLUMN", "column": {
"columnName": "email", "columnType": "VARCHAR",
"columnLength": 255, "isNullable": True, "columnComment": "邮箱"}}],
}), mode="roundtrip")
record("alter_database", await call("alter_database", {
"connectionId": ddl_conn, "databaseName": PREFIX + "db2",
"newName": PREFIX + "db2_renamed",
}), mode="roundtrip")
else:
for n in ("create_database", "create_table", "create_database_table",
"alter_table", "alter_database"):
RESULTS.append((n, "SKIP", "no builtin connection for DDL"))
# ---- 5. find the table id, exercise execute_sql + builtin CRUD ------
print("\n=== SQL 执行 + 内置表数据 CRUD ===")
# locate a datasource config id + table id for our table
cfgs = await call("list_datasource_configs", {"datasourceName": PREFIX, "pageSize": 20})
cfg_rows = cfgs.get("rows") or dig(cfgs, "data", "rows", default=[]) or []
datasource_id = cfg_rows[0].get("id") if cfg_rows else None
state["datasource_id"] = datasource_id
table_id = None
if ddl_conn:
detail = await call("get_connection", {"id": ddl_conn})
for ds in dig(detail, "data", "datasourceConfig", default=[]) or []:
for t in ds.get("tables", []) or []:
if str(t.get("tableName", "")).startswith(PREFIX):
table_id = t.get("tableId") or t.get("id")
break
if table_id:
break
state["table_id"] = table_id
print(f" -> datasource_id={datasource_id}, table_id={table_id}")
if datasource_id:
record("execute_sql", await call("execute_sql", {
"datasourceId": datasource_id,
"sql": "SELECT 1",
"databaseName": db_name,
}), mode="roundtrip")
else:
RESULTS.append(("execute_sql", "SKIP", "no datasource config id"))
if table_id:
record("builtin_table_insert", await call("builtin_table_insert", {
"tableId": table_id, "data": {"id": 1, "name": "张三", "age": 25},
}), mode="roundtrip")
record("builtin_table_data", await call("builtin_table_data", {
"tableId": table_id,
}), mode="roundtrip")
record("builtin_table_update", await call("builtin_table_update", {
"tableId": table_id, "data": {"name": "李四", "age": 30},
"primaryKey": {"id": 1},
}), mode="roundtrip")
record("builtin_table_delete", await call("builtin_table_delete", {
"tableId": table_id, "primaryKeys": [{"id": 1}],
}), mode="roundtrip")
else:
for n in ("builtin_table_insert", "builtin_table_data",
"builtin_table_update", "builtin_table_delete"):
RESULTS.append((n, "SKIP", "no table id found"))
# ---- 6. AI generate + document import (preview/confirm) -------------
print("\n=== AI 生成 + 文档导入 ===")
record("generate_table", await call("generate_table", {
"requirement": "一个简单的待办事项表,含标题、状态、创建时间",
"databaseId": datasource_id,
}), mode="roundtrip")
# build a tiny CSV for import preview
csv_path = os.path.join(tempfile.gettempdir(), "selftest_import.csv")
with open(csv_path, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["name", "age", "gender"])
w.writerow(["张三", "25", "male"])
w.writerow(["李四", "30", "female"])
preview = None
if ddl_conn:
preview = record("import_document_preview", await call("import_document_preview", {
"connectionId": ddl_conn, "filePath": csv_path,
}), mode="roundtrip")
ts = dig(preview, "data", "tableStructure")
all_data = dig(preview, "data", "allData")
if ts and all_data:
record("import_document_confirm", await call("import_document_confirm", {
"connectionId": ddl_conn, "tableStructure": ts, "allData": all_data,
}), mode="roundtrip")
else:
RESULTS.append(("import_document_confirm", "SKIP",
"preview returned no tableStructure/allData"))
else:
for n in ("import_document_preview", "import_document_confirm"):
RESULTS.append((n, "SKIP", "no builtin connection"))
# ---- 7. datasource config batch ops --------------------------------
print("\n=== 数据源配置批量操作 ===")
if ddl_conn:
record("batch_create_datasource_configs", await call(
"batch_create_datasource_configs", {
"connectionId": ddl_conn,
"datasourceNamePrefix": PREFIX + "cfg",
"syncTables": False,
"databases": [{"databaseName": db_name, "tableNames": []}],
}), mode="roundtrip")
else:
RESULTS.append(("batch_create_datasource_configs", "SKIP", "no connection"))
if datasource_id:
record("get_datasource_config", await call("get_datasource_config", {
"id": datasource_id}), mode="roundtrip")
record("change_datasource_status", await call("change_datasource_status", {
"id": datasource_id, "status": 1}), mode="roundtrip")
record("batch_update_datasource_configs", await call(
"batch_update_datasource_configs", {
"syncTables": False,
"datasources": [{"id": datasource_id, "tableNames": []}],
}), mode="roundtrip")
else:
for n in ("get_datasource_config", "change_datasource_status",
"batch_update_datasource_configs"):
RESULTS.append((n, "SKIP", "no datasource config id"))
record("export_datasource_configs", await call("export_datasource_configs", {
"datasourceName": PREFIX}), mode="roundtrip")
# replace_datasource_configs intentionally skipped (destructive)
RESULTS.append(("replace_datasource_configs", "SKIP",
"destructive full-replace, skipped by design"))
# ---- 8. cleanup -----------------------------------------------------
print("\n=== 清理 selftest_ 资源 ===")
# delete selftest datasource configs
cfgs2 = await call("list_datasource_configs", {"datasourceName": PREFIX, "pageSize": 50})
cfg_rows2 = cfgs2.get("rows") or dig(cfgs2, "data", "rows", default=[]) or []
del_ids = [r.get("id") for r in cfg_rows2 if r.get("id") is not None]
if del_ids:
record("delete_datasource_configs", await call("delete_datasource_configs", {
"ids": del_ids}), mode="roundtrip")
else:
RESULTS.append(("delete_datasource_configs", "SKIP", "nothing to delete"))
# delete selftest connections
deleted_conn = False
for cid in (state.get("ext_id"), conn_id):
if cid:
record("delete_connection", await call("delete_connection", {"id": cid}),
mode="roundtrip")
deleted_conn = True
if not deleted_conn:
RESULTS.append(("delete_connection", "SKIP", "no selftest connection to delete"))
try:
os.remove(csv_path)
except OSError:
pass
try:
stop_mock_db()
except Exception as e: # noqa: BLE001
print(f" -> failed to stop mock DB: {e}")
# ---- summary --------------------------------------------------------
print("\n" + "=" * 60)
print("自测结果汇总")
print("=" * 60)
all_names = {t["name"] for t in ALL_TOOLS}
counts = {"PASS": 0, "FAIL": 0, "SKIP": 0}
for name, status, detail in RESULTS:
counts[status] = counts.get(status, 0) + 1
for name, status, detail in RESULTS:
print(f" [{status}] {name}: {detail}")
untested = all_names - TESTED
print("-" * 60)
print(f"工具总数: {len(all_names)} 覆盖: {len(TESTED)} 未触达: {sorted(untested)}")
print(f"PASS={counts['PASS']} FAIL={counts['FAIL']} SKIP={counts['SKIP']}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,4 @@
[[index]]
name = "pypi"
url = "https://pypi.org/simple/"
default = true

View File

@@ -1,6 +1,6 @@
Metadata-Version: 2.4 Metadata-Version: 2.4
Name: lzwcai-mcp-api-converter Name: lzwcai-mcp-api-converter
Version: 0.2.0 Version: 0.2.5
Summary: 基于FastMCP框架的动态API工具服务器自动将企业业务API配置转换为MCP协议工具支持多种传输方式、企业认证和参数验证为AI助手提供标准化的业务接口访问能力。 Summary: 基于FastMCP框架的动态API工具服务器自动将企业业务API配置转换为MCP协议工具支持多种传输方式、企业认证和参数验证为AI助手提供标准化的业务接口访问能力。
Requires-Python: >=3.10 Requires-Python: >=3.10
Description-Content-Type: text/markdown Description-Content-Type: text/markdown

View File

@@ -1,37 +1,104 @@
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================ 2026-03-18 17:57:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2025-12-30 11:48:23 2026-03-18 17:57:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 17:57:40
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO 2026-03-18 17:57:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log 2026-03-18 17:57:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False 2026-03-18 17:57:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True 2026-03-18 17:57:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份 2026-03-18 17:57:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================ 2026-03-18 17:57:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:277] - 开始初始化 MCP 服务器 2026-03-18 17:57:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:116] - 配置模式: memory 2026-03-18 17:57:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 17:57:52
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:124] - 使用内存模式加载配置(多租户支持) 2026-03-18 17:57:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:135] - 使用环境变量提供的businessUuid: u9ua9ss2l8c 2026-03-18 17:57:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:139] - 租户配置变量名: businessu9ua9ss2l8c 2026-03-18 17:57:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:147] - 内存中没有租户 u9ua9ss2l8c 的配置,开始从业务平台获取... 2026-03-18 17:57:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:168] - 从环境变量bizSysApiIds获取到API IDs: [1970386761072058369, 1970386761185304578, 1970386761583763457, 1970386761420185602] 2026-03-18 17:57:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:175] - 调用get_business_api_config获取配置API IDs: [1970386761072058369, 1970386761185304578, 1970386761583763457, 1970386761420185602] 2026-03-18 17:57:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.business.get_business_api - INFO - [get_business_api.py:193] - 开始获取 4 个API的详情... 2026-03-18 17:58:11 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.business.get_business_api - INFO - [get_business_api.py:93] - 成功获取 4 个API详情 2026-03-18 17:58:11 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 17:58:11
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.business.get_business_api - INFO - [get_business_api.py:197] - 开始映射为配置格式... 2026-03-18 17:58:11 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.business.get_business_api - INFO - [get_business_api.py:158] - 成功映射 4 个API到配置格式 2026-03-18 17:58:11 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.business.get_business_api - INFO - [get_business_api.py:159] - 服务名称: lzwcai_mcp_api_converter 2026-03-18 17:58:11 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.business.get_business_api - INFO - [get_business_api.py:160] - 域名URL: https://erp.166bg.com/api 2026-03-18 17:58:11 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.business.get_business_api - INFO - [get_business_api.py:161] - 描述: 定时任务列表、定时任务详情、任务列表、任务统计列表(按状态) 2026-03-18 17:58:11 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.business.get_business_api - INFO - [get_business_api.py:200] - [SUCCESS] 成功生成API配置包含 4 个API 2026-03-18 17:58:11 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:177] - 成功获取业务API配置包含 4 个API配置 2026-03-18 17:58:59 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:181] - 配置已存储到内存变量: businessu9ua9ss2l8c 2026-03-18 17:58:59 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 17:58:59
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:182] - 当前内存中共有 1 个租户配置 2026-03-18 17:58:59 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:292] - 服务器配置 - 名称: lzwcai-mcp-dyntoolapi, 版本: 1.0.0 2026-03-18 17:58:59 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.core.api_base - INFO - [api_base.py:262] - 开始处理 4 个API配置 2026-03-18 17:58:59 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.core.api_base - INFO - [api_base.py:317] - API配置处理完成成功处理 4 个配置 2026-03-18 17:58:59 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.core.api_base - INFO - [api_base.py:389] - ApiBase初始化完成共处理 4 个API配置 2026-03-18 17:58:59 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:307] - API 基础服务初始化完成,共 4 个API配置 2026-03-18 17:58:59 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:506] - 注册API工具插件 2026-03-18 17:59:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:509] - API工具插件注册完成 2026-03-18 17:59:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 17:59:12
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:779] - 配置文件监控功能已禁用如需启用请设置环境变量ENABLE_CONFIG_WATCH=true 2026-03-18 17:59:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2025-12-30 11:48:23 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:578] - 启动STDIO传输模式 2026-03-18 17:59:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2025-12-30 11:48:24 - lzwcai_mcp_api_converter.src.create_mcp - INFO - [create_mcp.py:391] - 返回工具列表,共 4 个工具 2026-03-18 17:59:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2026-03-18 17:59:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2026-03-18 17:59:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2026-03-18 17:59:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2026-03-18 18:04:50 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2026-03-18 18:04:50 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 18:04:50
2026-03-18 18:04:50 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2026-03-18 18:04:50 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2026-03-18 18:04:50 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2026-03-18 18:04:50 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2026-03-18 18:04:50 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2026-03-18 18:04:50 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2026-03-18 18:06:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2026-03-18 18:06:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 18:06:12
2026-03-18 18:06:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2026-03-18 18:06:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2026-03-18 18:06:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2026-03-18 18:06:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2026-03-18 18:06:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2026-03-18 18:06:12 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2026-03-18 18:10:29 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2026-03-18 18:10:29 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 18:10:29
2026-03-18 18:10:29 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2026-03-18 18:10:29 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2026-03-18 18:10:29 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2026-03-18 18:10:29 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2026-03-18 18:10:29 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2026-03-18 18:10:29 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2026-03-18 18:10:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2026-03-18 18:10:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 18:10:40
2026-03-18 18:10:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2026-03-18 18:10:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2026-03-18 18:10:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2026-03-18 18:10:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2026-03-18 18:10:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2026-03-18 18:10:40 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2026-03-18 18:11:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2026-03-18 18:11:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 18:11:20
2026-03-18 18:11:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2026-03-18 18:11:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2026-03-18 18:11:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2026-03-18 18:11:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2026-03-18 18:11:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2026-03-18 18:11:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2026-03-18 18:13:17 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2026-03-18 18:13:17 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 18:13:17
2026-03-18 18:13:17 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2026-03-18 18:13:17 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2026-03-18 18:13:17 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2026-03-18 18:13:17 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2026-03-18 18:13:17 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2026-03-18 18:13:17 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-03-18 18:16:52
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-06-16 10:55:20
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================

View File

@@ -1 +0,0 @@
lzwc19781970385781825785858token={"authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMTAwMDAwMDEiLCJsb2dpbl91c2VyX2tleSI6IjJmNmViMWVkYTk3MGRlNzI1OTM1YTczNzY5YWZmODJmZDE3MmFmMGIiLCJhYmJyIjoiXHU3MDc1XHU2Y2ZkXHU0ZTA3XHU1ZGRkIiwiYXVkIjoiIiwiZXhwIjoxNzY3MzQ4OTQxLCJpYXQiOjE3NjY3NDQxNDEsImlzcyI6IiIsImp0aSI6IjUyOTIyNzc0ZTdmZDA3MjZkNGEyY2FkMTgyYzEzNjM4IiwibmJmIjoxNzY2NzQ0MTQxLCJzdWIiOiIifQ.S8cvKtUfojJu0JvA1aPgd6H9y5ccd7XOa7UHMqZzn5w"}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,219 @@
{
"serverName": "lzwcai_mcp_api_converter",
"description": "登录、单据查询",
"domainUrl": "http://39.108.116.74",
"packageName": "lzwcai-mcp-dyntoolapi",
"version": "1.0.0",
"apiConfig": [
{
"id": "2029506334288154626",
"enterpriseId": "1932095424144715777",
"bizSysId": "2029468454441897985",
"domainUrl": "http://39.108.116.74",
"interfaceName": "登录",
"businessPrompts": "登录",
"returnType": "JSON",
"returnConversion": "{\"success_param\": \"code==200\", \"status_param\": \"code\", \"msg_param\": \"msg\", \"data\": \"data\"}",
"header": null,
"apiUrl": "/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc",
"parametersFormat": "2",
"method": "POST",
"status": 1,
"version": "1.0.0",
"authenticationRequired": 0,
"responseExample": null,
"crudType": "0",
"isView": 0,
"templateType": "markdown",
"viewTemplates": null,
"parameters": [
{
"keyParam": null,
"required": 1,
"paramName": "parameters",
"paramType": "ARRAY",
"paramPrompts": "",
"defaultValue": null,
"assoKey": null,
"assoApiId": null,
"memory": 0,
"apiId": "2029506334288154626",
"requestType": "body",
"dataFormat": null,
"validity": null,
"sort": "1",
"tags": "2",
"example": null
}
]
},
{
"id": "2029506334388817922",
"enterpriseId": "1932095424144715777",
"bizSysId": "2029468454441897985",
"domainUrl": "http://39.108.116.74",
"interfaceName": "单据查询",
"businessPrompts": "单据查询",
"returnType": "JSON",
"returnConversion": "{\"success_param\": \"code==200\", \"status_param\": \"code\", \"msg_param\": \"msg\", \"data\": \"data\"}",
"header": null,
"apiUrl": "/k3cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.ExecuteBillQuery.common.kdsvc",
"parametersFormat": "2",
"method": "POST",
"status": 1,
"version": "1.0.0",
"authenticationRequired": 1,
"responseExample": null,
"crudType": "4",
"isView": 0,
"templateType": "markdown",
"viewTemplates": null,
"parameters": [
{
"keyParam": null,
"required": 1,
"paramName": "parameters",
"paramType": "ARRAY",
"paramPrompts": "",
"defaultValue": null,
"assoKey": null,
"assoApiId": null,
"memory": 0,
"apiId": "2029506334388817922",
"requestType": "body",
"dataFormat": null,
"validity": null,
"sort": "1",
"tags": "2",
"example": null
},
{
"keyParam": null,
"required": 0,
"paramName": "parameters[].FormId",
"paramType": "STRING",
"paramPrompts": "",
"defaultValue": null,
"assoKey": null,
"assoApiId": null,
"memory": 0,
"apiId": "2029506334388817922",
"requestType": "body",
"dataFormat": null,
"validity": null,
"sort": "2",
"tags": "2",
"example": null
},
{
"keyParam": null,
"required": 0,
"paramName": "parameters[].TopRowCount",
"paramType": "INTEGER",
"paramPrompts": "",
"defaultValue": null,
"assoKey": null,
"assoApiId": null,
"memory": 0,
"apiId": "2029506334388817922",
"requestType": "body",
"dataFormat": null,
"validity": null,
"sort": "3",
"tags": "2",
"example": null
},
{
"keyParam": null,
"required": 0,
"paramName": "parameters[].Limit",
"paramType": "INTEGER",
"paramPrompts": "",
"defaultValue": null,
"assoKey": null,
"assoApiId": null,
"memory": 0,
"apiId": "2029506334388817922",
"requestType": "body",
"dataFormat": null,
"validity": null,
"sort": "4",
"tags": "1",
"example": null
},
{
"keyParam": null,
"required": 0,
"paramName": "parameters[].StartRow",
"paramType": "INTEGER",
"paramPrompts": "",
"defaultValue": null,
"assoKey": null,
"assoApiId": null,
"memory": 0,
"apiId": "2029506334388817922",
"requestType": "body",
"dataFormat": null,
"validity": null,
"sort": "5",
"tags": "2",
"example": null
},
{
"keyParam": null,
"required": 0,
"paramName": "parameters[].FilterString",
"paramType": "STRING",
"paramPrompts": "",
"defaultValue": null,
"assoKey": null,
"assoApiId": null,
"memory": 0,
"apiId": "2029506334388817922",
"requestType": "body",
"dataFormat": null,
"validity": null,
"sort": "6",
"tags": "2",
"example": null
},
{
"keyParam": null,
"required": 0,
"paramName": "parameters[].OrderString",
"paramType": "STRING",
"paramPrompts": "",
"defaultValue": null,
"assoKey": null,
"assoApiId": null,
"memory": 0,
"apiId": "2029506334388817922",
"requestType": "body",
"dataFormat": null,
"validity": null,
"sort": "7",
"tags": "3",
"example": null
},
{
"keyParam": null,
"required": 0,
"paramName": "parameters[].FieldKeys",
"paramType": "STRING",
"paramPrompts": "",
"defaultValue": null,
"assoKey": null,
"assoApiId": null,
"memory": 0,
"apiId": "2029506334388817922",
"requestType": "body",
"dataFormat": null,
"validity": null,
"sort": "8",
"tags": "0",
"example": null
}
]
}
]
}

View File

@@ -687,7 +687,7 @@ class AuthService:
self, self,
user_id: Optional[str], user_id: Optional[str],
biz_sys_id: Optional[str], biz_sys_id: Optional[str],
persist_token: bool = True, persist_token: bool = False,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
完整的请求鉴权处理 完整的请求鉴权处理
@@ -725,7 +725,7 @@ class AuthService:
user_id: Optional[str], user_id: Optional[str],
biz_sys_id: Optional[str], biz_sys_id: Optional[str],
token: Optional[str] = None, token: Optional[str] = None,
persist_token: bool = True, persist_token: bool = False,
) -> Optional[Union[str, Dict[str, Any]]]: ) -> Optional[Union[str, Dict[str, Any]]]:
""" """
检查用户Token是否有效如无效则重新获取 检查用户Token是否有效如无效则重新获取
@@ -760,7 +760,7 @@ class AuthService:
# 如果环境变量存在,直接返回值 # 如果环境变量存在,直接返回值
if exists: if exists:
self.logger.info( self.logger.info(
f"从环境变量获取到用户{user_id}业务系统{biz_sys_id}的Token" f"从环境变量获取到用户{user_id}业务系统{biz_sys_id}的Token: {token_value}"
) )
return token_value return token_value
@@ -774,7 +774,7 @@ class AuthService:
return await self._refresh_user_token(user_id, biz_sys_id, token_name, persist_token) return await self._refresh_user_token(user_id, biz_sys_id, token_name, persist_token)
async def _refresh_user_token( async def _refresh_user_token(
self, user_id: str, biz_sys_id: str, token_name: str, persist_token: bool = True self, user_id: str, biz_sys_id: str, token_name: str, persist_token: bool = False
) -> Optional[Union[str, Dict[str, Any]]]: ) -> Optional[Union[str, Dict[str, Any]]]:
"""刷新用户Token""" """刷新用户Token"""
# 获取鉴权类型和认证数据 # 获取鉴权类型和认证数据

View File

@@ -667,7 +667,9 @@ class ApiClient:
auth_result.get("error_response", {}).get("status_code"), auth_result.get("error_response", {}).get("status_code"),
) )
return auth_result.get("tokenHeader", {}) token_header = auth_result.get("tokenHeader", {})
logger.info(f"API调用获取到Token - 用户ID: {user_id}, 业务系统ID: {biz_sys_id}, Token: {token_header}")
return token_header
def _contains_file(self, data: Dict[str, Any]) -> bool: def _contains_file(self, data: Dict[str, Any]) -> bool:
@@ -938,10 +940,28 @@ class ApiClient:
"status_code": response.status_code, "status_code": response.status_code,
} }
else: else:
# 对于非JSON响应记录前100个字符 # 对于非JSON响应尝试解析为JSON某些API返回text/plain但内容是JSON
if not response.text:
return {
"status": "success",
"data": "",
"status_code": response.status_code,
}
response_preview = response.text[:100] + "..." if len(response.text) > 100 else response.text response_preview = response.text[:100] + "..." if len(response.text) > 100 else response.text
logger.info(f"文本响应预览: {response_preview}") logger.info(f"文本响应预览: {response_preview}")
# 尝试解析为JSON
text = response.text.strip()
if text and (text.startswith('{') or text.startswith('[')):
try:
json_response = json.loads(text)
logger.info("文本响应成功解析为JSON")
return json_response
except (json.JSONDecodeError, ValueError) as e:
logger.debug(f"文本响应JSON解析失败: {e}")
# 返回原始文本
return { return {
"status": "success", "status": "success",
"data": response.text, "data": response.text,

View File

@@ -138,13 +138,11 @@ def load_api_configs():
config_key = f"business{business_uuid}" config_key = f"business{business_uuid}"
logger.info(f"租户配置变量名: {config_key}") logger.info(f"租户配置变量名: {config_key}")
# 检查内存中是否已有该租户的配置 # 构建租户专属的配置文件路径
if config_key in business_configs: memory_config_path = os.path.join(current_dir, f"api_config_{business_uuid}.json")
logger.info(f"从内存中获取租户 {business_uuid} 的配置")
return business_configs[config_key]
# 内存中没有,从业务平台获取 # 本地文件不存在或加载失败,从业务平台获取
logger.info(f"内存中没有租户 {business_uuid}配置,开始从业务平台获取...") logger.info(f"准备从业务平台获取租户 {business_uuid}最新配置(强制刷新)...")
try: try:
# 从环境变量获取API ID列表 # 从环境变量获取API ID列表
@@ -181,11 +179,35 @@ def load_api_configs():
logger.info(f"配置已存储到内存变量: {config_key}") logger.info(f"配置已存储到内存变量: {config_key}")
logger.info(f"当前内存中共有 {len(business_configs)} 个租户配置") logger.info(f"当前内存中共有 {len(business_configs)} 个租户配置")
# 保存到本地文件作为备份
try:
logger.info(f"保存配置到本地文件: {memory_config_path}")
with open(memory_config_path, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
logger.info("配置文件保存成功")
except Exception as save_error:
logger.warning(f"保存配置到本地文件失败(不影响运行): {str(save_error)}")
return config return config
except Exception as e: except Exception as e:
logger.error(f"获取业务API配置失败: {str(e)}") logger.error(f"获取业务API配置失败: {str(e)}")
error_msg = f"内存模式下无法获取租户 {business_uuid} 的配置: {str(e)}"
# 网络获取失败,尝试降级使用本地缓存
if os.path.exists(memory_config_path):
try:
logger.info(f"网络获取失败,尝试使用本地缓存文件: {memory_config_path}")
with open(memory_config_path, "r", encoding="utf-8") as f:
config = json.load(f)
logger.info(f"成功加载本地缓存配置,包含 {len(config.get('apiConfig', []))} 个API配置")
# 存储到内存中
business_configs[config_key] = config
return config
except Exception as cache_error:
logger.error(f"加载本地缓存也失败了: {str(cache_error)}")
error_msg = f"无法获取租户 {business_uuid} 的配置(网络和缓存均不可用): {str(e)}"
raise Exception(error_msg) raise Exception(error_msg)
# ==================== 模式二:文件模式(单租户) ==================== # ==================== 模式二:文件模式(单租户) ====================
@@ -367,24 +389,12 @@ class ApiToolPlugin(ToolPlugin):
tool_name = tool_config["interfaceName"] # 工具名称(拼音格式) tool_name = tool_config["interfaceName"] # 工具名称(拼音格式)
logger.debug(f"注册工具: {tool_name}") logger.debug(f"注册工具: {tool_name}")
# 定义输出 Schema先写死一个测试
output_schema = {
"type": "object",
"properties": {
"code": {"type": "integer", "description": "响应状态码0表示成功"},
"message": {"type": "string", "description": "响应消息"},
"data": {"type": "object", "description": "响应数据"}
},
"required": ["code", "message"]
}
# 创建MCP工具定义 # 创建MCP工具定义
tools.append( tools.append(
types.Tool( types.Tool(
name=tool_name, # 工具名称 name=tool_name,
description=tool_config["schema_description"], # 工具描述(包含参数说明) description=tool_config["schema_description"],
inputSchema=tool_config["schema"], # 输入参数的JSON Schema inputSchema=tool_config["schema"],
outputSchema=output_schema, # 输出参数的JSON Schema
) )
) )
@@ -639,7 +649,7 @@ def refresh_api_configs():
这个函数实现了配置的热加载功能,支持两种模式: 这个函数实现了配置的热加载功能,支持两种模式:
- 文件模式:当检测到配置文件变化时会被调用 - 文件模式:当检测到配置文件变化时会被调用
- 内存模式:强制重新从业务平台获取配置并更新内存 - 内存模式:强制重新从业务平台获取配置并更新内存和本地文件
全局变量更新: 全局变量更新:
- api_configs: 重新加载的API配置 - api_configs: 重新加载的API配置
@@ -658,7 +668,7 @@ def refresh_api_configs():
# 获取配置模式 # 获取配置模式
config_mode = os.getenv('configMode', 'memory').lower() config_mode = os.getenv('configMode', 'memory').lower()
# 内存模式下需要清除当前租户的缓存配置,强制重新获取 # 内存模式下需要清除当前租户的缓存配置和本地文件,强制重新获取
if config_mode == 'memory': if config_mode == 'memory':
business_uuid = os.getenv('businessUuid') business_uuid = os.getenv('businessUuid')
if business_uuid: if business_uuid:
@@ -667,6 +677,16 @@ def refresh_api_configs():
logger.info(f"清除租户 {business_uuid} 的缓存配置") logger.info(f"清除租户 {business_uuid} 的缓存配置")
del business_configs[config_key] del business_configs[config_key]
# 删除本地配置文件,强制从业务平台重新获取
current_dir = os.path.dirname(os.path.abspath(__file__))
memory_config_path = os.path.join(current_dir, f"api_config_{business_uuid}.json")
if os.path.exists(memory_config_path):
try:
os.remove(memory_config_path)
logger.info(f"已删除本地配置文件: {memory_config_path}")
except Exception as e:
logger.warning(f"删除本地配置文件失败: {str(e)}")
# 重新加载API配置 # 重新加载API配置
api_configs = load_api_configs() api_configs = load_api_configs()

View File

@@ -1,9 +1,9 @@
import os import os
os.environ["modelId"] = "1946471611735015425" os.environ["modelId"] = "1946471611735015425"
os.environ["bizSysId"] = "1970385781825785858" os.environ["bizSysId"] = "2029468454441897985"
os.environ["bizSysApiIds"] = "[\"1970386761072058369\",\"1970386761185304578\",\"1970386761583763457\",\"1970386761420185602\"]" os.environ["bizSysApiIds"] = "[\"2033382693160300546\"]"
os.environ["businessUuid"] = "u9ua9ss2l8c" os.environ["businessUuid"] = "dcqwlucfo7h"
os.environ["LZWCAI_CORP_MANAGER_URL"] = "http://192.168.2.236:8088" os.environ["LZWCAI_CORP_MANAGER_URL"] = "http://192.168.2.236:8088"
# 导入模块 # 导入模块
from lzwcai_mcp_api_converter.src.create_mcp import run_main from lzwcai_mcp_api_converter.src.create_mcp import run_main

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "lzwcai-mcp-api-converter" name = "lzwcai-mcp-api-converter"
version = "0.2.0" version = "0.2.5"
description = "基于FastMCP框架的动态API工具服务器自动将企业业务API配置转换为MCP协议工具支持多种传输方式、企业认证和参数验证为AI助手提供标准化的业务接口访问能力。" description = "基于FastMCP框架的动态API工具服务器自动将企业业务API配置转换为MCP协议工具支持多种传输方式、企业认证和参数验证为AI助手提供标准化的业务接口访问能力。"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"

View File

@@ -1,220 +1,208 @@
2025-10-23 15:05:51 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs 2026-05-25 16:05:46 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs
2025-10-23 15:05:51 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5 2026-05-25 16:05:46 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5
2025-10-23 15:05:51 - mcp_services - INFO - [main.py:341] - 开始运行 MCP SQL Executor 服务器 2026-05-25 16:05:46 - mcp_services - INFO - [main.py:352] - 开始运行 MCP SQL Executor 服务器
2025-10-23 15:05:51 - mcp_services - INFO - [main.py:293] - ============================================================ 2026-05-25 16:05:46 - mcp_services - INFO - [main.py:304] - ============================================================
2025-10-23 15:05:51 - mcp_services - INFO - [main.py:294] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor 2026-05-25 16:05:46 - mcp_services - INFO - [main.py:305] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor
2025-10-23 15:05:51 - mcp_services - INFO - [main.py:295] - 版本: 0.1.0 2026-05-25 16:05:46 - mcp_services - INFO - [main.py:306] - 版本: 0.1.0
2025-10-23 15:05:51 - mcp_services - INFO - [main.py:296] - ============================================================ 2026-05-25 16:05:46 - mcp_services - INFO - [main.py:307] - ============================================================
2025-10-23 15:05:51 - mcp_services - INFO - [main.py:300] - 环境配置 - Database ID: 29 2026-05-25 16:05:46 - mcp_services - INFO - [main.py:311] - 环境配置 - Database ID: 240
2025-10-23 15:05:51 - mcp_services - INFO - [main.py:301] - 环境配置 - Skill ID: 1981195682443014146 2026-05-25 16:05:46 - mcp_services - INFO - [main.py:312] - 环境配置 - Skill ID: 2058819964077572098
2025-10-23 15:05:51 - mcp_services - INFO - [main.py:302] - 环境配置 - Backend Base URL: http://192.168.2.236:8088 2026-05-25 16:05:46 - mcp_services - INFO - [main.py:313] - 环境配置 - Backend Base URL: http://192.168.2.236:8088
2025-10-23 15:05:51 - mcp_services - INFO - [main.py:303] - ============================================================ 2026-05-25 16:05:46 - mcp_services - INFO - [main.py:314] - ============================================================
2025-10-23 15:05:51 - mcp_services - INFO - [main.py:308] - MCP 服务器已启动,等待客户端连接... 2026-05-25 16:05:46 - mcp_services - INFO - [main.py:319] - MCP 服务器已启动,等待客户端连接...
2025-10-23 15:05:52 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest 2026-05-25 16:05:49 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2025-10-23 15:05:52 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 2026-05-25 16:05:49 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2025-10-23 15:05:52 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api... 2026-05-25 16:05:49 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api...
2025-10-23 15:05:52 - mcp_services - INFO - [main.py:272] - 调用第三方APIskill_id: 1981195682443014146 2026-05-25 16:05:49 - mcp_services - INFO - [main.py:283] - 调用第三方APIskill_id: 2058819964077572098
2025-10-23 15:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981195682443014146 2026-05-25 16:05:49 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://192.168.2.236:8088/datasource/skill/getBySkillId/2058819964077572098
2025-10-23 15:05:52 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/datasource/skill/getBySkillId/1981195682443014146 "HTTP/1.1 200 " 2026-05-25 16:05:49 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/datasource/skill/getBySkillId/2058819964077572098 "HTTP/1.1 200 "
2025-10-23 15:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:85] - API调用成功: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981195682443014146 2026-05-25 16:05:49 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:85] - API调用成功: http://192.168.2.236:8088/datasource/skill/getBySkillId/2058819964077572098
2025-10-23 15:05:52 - mcp_services - INFO - [main.py:277] - 成功{'msg': '查询失败: 技能不存在: 1981195682443014146', 'code': 500} 2026-05-25 16:05:49 - mcp_services - INFO - [main.py:288] - 成功{'msg': '查询成功', 'code': 200, 'data': [{'id': '2058819964287287298', 'createBy': 'wxl06', 'createTime': '2026-05-25 15:58:25', 'updateBy': None, 'updateTime': None, 'serviceId': '2058819964085960705', 'uniqueName': '前50条导入数据明细查询', 'name': 'qian50tiaodaorushujumingxichaxun_32b1d628', 'description': 'qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: 查询数据导入明细表中的前50条记录包含原始数据、处理状态、错误原因等关键信息用于快速预览最近的导入情况。', 'visualizable': 1, 'toolPrompt': '查询成功,返回 6 行数据,执行时间: 1ms', 'toolType': 'sql', 'datasourceId': '240', 'sqlTemplate': 'SELECT record_id AS "记录ID", task_id AS "任务ID", original_data AS "原始数据", process_result AS "处理结果", error_reason AS "错误原因", process_status AS "处理状态", target_table AS "关联目标表", created_time AS "创建时间" FROM import_record_detail LIMIT 50;', 'sqlParams': '{}', 'resultType': 'list', 'sourceType': 'ai', 'trainingTaskId': None, 'tableMetadataIds': '', 'executionCount': 0, 'visualizationConfigs': None, 'inputJsonSchema': '{"type":"object","properties":{},"required":[]}', 'outputJsonSchema': '{"type":"object","properties":{"text":{"type":"string"}},"additionalProperties":false}', 'lastExecutionTime': None}]}
2025-10-23 15:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:297] - 成功处理 0 条技能数据 2026-05-25 16:05:49 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:324] - 成功处理 1 条技能数据
2025-10-23 15:05:52 - mcp_services - INFO - [main.py:282] - 成功获取并处理 0 条数据 2026-05-25 16:05:49 - mcp_services - INFO - [main.py:293] - 成功获取并处理 1 条数据
2025-10-23 15:05:52 - mcp_services - INFO - [main.py:128] - API配置: 0 2026-05-25 16:05:49 - mcp_services - INFO - [main.py:128] - API配置: 1
2025-10-23 15:05:52 - mcp_services - INFO - [main.py:129] - API配置数组: [] 2026-05-25 16:05:49 - mcp_services - INFO - [main.py:129] - API配置数组: [{'id': '2058819964287287298', 'businessName': 'qian50tiaodaorushujumingxichaxun_32b1d628', 'businessDescription': 'qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: 查询数据导入明细表中的前50条记录包含原始数据、处理状态、错误原因等关键信息用于快速预览最近的导入情况。', 'sqlTemplate': 'SELECT record_id AS "记录ID", task_id AS "任务ID", original_data AS "原始数据", process_result AS "处理结果", error_reason AS "错误原因", process_status AS "处理状态", target_table AS "关联目标表", created_time AS "创建时间" FROM import_record_detail LIMIT 50;', 'parameters': {}, 'datasourceId': '240'}]
2025-10-23 15:05:52 - mcp_services - INFO - [main.py:165] - 成功生成 0 个 MCP 工具 2026-05-25 16:05:49 - mcp_services - INFO - [main.py:165] - 成功生成 1 个 MCP 工具
2025-10-23 15:08:30 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs 2026-05-25 16:05:52 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2025-10-23 15:08:30 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5 2026-05-25 16:05:52 - mcp_services - INFO - [main.py:190] - 收到工具调用请求: qian50tiaodaorushujumingxichaxun_32b1d628
2025-10-23 15:08:30 - mcp_services - INFO - [main.py:341] - 开始运行 MCP SQL Executor 服务器 2026-05-25 16:05:52 - mcp_services - INFO - [main.py:230] - 正在调用测试SQL API...
2025-10-23 15:08:30 - mcp_services - INFO - [main.py:293] - ============================================================ 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:132] - ================================================================================
2025-10-23 15:08:30 - mcp_services - INFO - [main.py:294] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:133] - test_sql_with_schema 接口接收到的数据:
2025-10-23 15:08:30 - mcp_services - INFO - [main.py:295] - 版本: 0.1.0 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:134] - 数据类型: <class 'dict'>
2025-10-23 15:08:30 - mcp_services - INFO - [main.py:296] - ============================================================ 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:135] - 数据内容: {
2025-10-23 15:08:30 - mcp_services - INFO - [main.py:300] - 环境配置 - Database ID: 29 "datasourceId": "240",
2025-10-23 15:08:30 - mcp_services - INFO - [main.py:301] - 环境配置 - Skill ID: 1981245768471322626 "businessName": "qian50tiaodaorushujumingxichaxun_32b1d628",
2025-10-23 15:08:30 - mcp_services - INFO - [main.py:302] - 环境配置 - Backend Base URL: http://192.168.2.236:8088 "businessDescription": "qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: 查询数据导入明细表中的前50条记录包含原始数据、处理状态、错误原因等关键信息用于快速预览最近的导入情况。",
2025-10-23 15:08:30 - mcp_services - INFO - [main.py:303] - ============================================================ "sqlTemplate": "SELECT record_id AS \"记录ID\", task_id AS \"任务ID\", original_data AS \"原始数据\", process_result AS \"处理结果\", error_reason AS \"错误原因\", process_status AS \"处理状态\", target_table AS \"关联目标表\", created_time AS \"创建时间\" FROM import_record_detail LIMIT 50;",
2025-10-23 15:08:30 - mcp_services - INFO - [main.py:308] - MCP 服务器已启动,等待客户端连接... "parameters": {},
2025-10-23 15:08:31 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest "testParams": {}
2025-10-23 15:08:31 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 }
2025-10-23 15:08:31 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api... 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:136] - 数据源ID: 240
2025-10-23 15:08:31 - mcp_services - INFO - [main.py:272] - 调用第三方APIskill_id: 1981245768471322626 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:137] - 业务名称: qian50tiaodaorushujumingxichaxun_32b1d628
2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981245768471322626 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:138] - 业务描述: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: 查询数据导入明细表中的前50条记录包含原始数据、处理状态、错误原因等关键信息用于快速预览最近的导入情况。
2025-10-23 15:08:31 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/datasource/skill/getBySkillId/1981245768471322626 "HTTP/1.1 200 " 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:139] - SQL模板: SELECT record_id AS "记录ID", task_id AS "任务ID", original_data AS "原始数据", process_result AS "处理结果", error_reason AS "错误原因", process_status AS "处理状态", target_table AS "关联目标表", created_time AS "创建时间" FROM import_record_detail LIMIT 50;
2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:85] - API调用成功: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981245768471322626 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:140] - 参数定义: {}
2025-10-23 15:08:31 - mcp_services - INFO - [main.py:277] - 成功{'msg': '查询成功', 'code': 200, 'data': [{'id': '1981245768769118209', 'createBy': 'yy8z6', 'createTime': '2025-10-23 14:26:16', 'updateBy': None, 'updateTime': None, 'serviceId': '1981245768475516930', 'uniqueName': '数字员工技能描述-SQL服务_数字员工列表查询', 'name': '数字员工列表查询', 'description': '查询系统中的所有数字员工信息包括员工ID、名称、类型、状态等核心信息用于展示数字员工管理列表', 'visualizable': 1, 'toolPrompt': '查询数字员工列表', 'toolType': 'sql', 'datasourceId': '32', 'sqlTemplate': 'SELECT id, employee_id, name, employee_type, status, created_at, updated_at, description FROM digital_employees WHERE is_deleted = 0 ORDER BY created_at DESC', 'sqlParams': '{"type":"object","required":[],"properties":{}}', 'resultType': 'list', 'sourceType': 'ai', 'trainingTaskId': None, 'tableMetadataIds': '889,890,891,892,893,894,895,896', 'executionCount': 0, 'visualizationConfigs': None, 'inputJsonSchema': '{}', 'outputJsonSchema': '{"type":"object","properties":{"data":{"type":"array"}}}', 'lastExecutionTime': None}]} 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:141] - 测试参数: {}
2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:283] - 技能 数字员工列表查询 (ID: 1981245768769118209) 的sqlParams为空使用默认员工ID参数 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:144] - ================================================================================
2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:297] - 成功处理 1 条技能数据 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:153] - 正在调用测试SQL API: http://192.168.2.236:8088/datasource/sqlExecutionLog/testSqlWithSchema
2025-10-23 15:08:31 - mcp_services - INFO - [main.py:282] - 成功获取并处理 1 条数据 2026-05-25 16:05:52 - httpx - INFO - [_client.py:1025] - HTTP Request: POST http://192.168.2.236:8088/datasource/sqlExecutionLog/testSqlWithSchema "HTTP/1.1 200 "
2025-10-23 15:08:31 - mcp_services - INFO - [main.py:128] - API配置: 1 条 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:170] - ================================================================================
2025-10-23 15:08:31 - mcp_services - INFO - [main.py:129] - API配置数组: [{'id': '1981245768769118209', 'businessName': '数字员工列表查询', 'businessDescription': '查询系统中的所有数字员工信息包括员工ID、名称、类型、状态等核心信息用于展示数字员工管理列表', 'sqlTemplate': 'SELECT id, employee_id, name, employee_type, status, created_at, updated_at, description FROM digital_employees WHERE is_deleted = 0 ORDER BY created_at DESC', 'parameters': {'type': 'object', 'required': ['employeeId'], 'properties': {'employeeId': {'type': 'number', 'description': '员工ID用于标识员工的唯一数字标识符', 'examples': [1001, 2002]}}}, 'datasourceId': '32'}] 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:171] - test_sql_with_schema 接口返回的数据:
2025-10-23 15:08:31 - mcp_services - INFO - [main.py:165] - 成功生成 1 个 MCP 工具 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:172] - HTTP状态码: 200
2025-12-31 12:57:27 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:173] - 响应数据类型: <class 'dict'>
2025-12-31 12:57:27 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:174] - 响应数据内容: {
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:347] - 开始运行 MCP SQL Executor 服务器 "msg": "操作成功",
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:299] - ============================================================ "code": 200,
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:300] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor "data": {
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:301] - 版本: 0.1.0 "resultCount": 6,
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:302] - ============================================================ "data": [
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:306] - 环境配置 - Database ID: 16 {
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:307] - 环境配置 - Skill ID: "记录ID": 80001,
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:308] - 环境配置 - Backend Base URL: http://192.168.11.24:8088 "任务ID": "3320260418001",
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:309] - ============================================================ "原始数据": "{\"name\": \"张三\", \"mobile\": \"13800000101\", \"dept\": \"销售部\", \"amount\": 1200}",
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:314] - MCP 服务器已启动,等待客户端连接... "处理结果": "导入成功",
2025-12-31 12:57:28 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest "错误原因": null,
2025-12-31 12:57:28 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 "处理状态": "SUCCESS",
2025-12-31 12:57:28 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api... "关联目标表": "employee_import",
2025-12-31 12:57:28 - mcp_services - INFO - [main.py:278] - 调用第三方APIskill_id: "创建时间": "2026-04-18T15:19:19.000+08:00"
2025-12-31 12:57:28 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://192.168.11.24:8088/datasource/skill/getBySkillId/ },
2025-12-31 12:57:29 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.11.24:8088/datasource/skill/getBySkillId/ "HTTP/1.1 404 " {
2025-12-31 12:57:29 - lzwcai_mcp_sqlexecutor.utils.api_client - ERROR - [api_client.py:97] - API请求失败 (HTTP 404): http://192.168.11.24:8088/datasource/skill/getBySkillId/ "记录ID": 80002,
2025-12-31 12:57:29 - lzwcai_mcp_sqlexecutor.utils.api_client - ERROR - [api_client.py:98] - 错误响应: {"timestamp":"2025-12-31T12:57:30.248+08:00","status":404,"error":"Not Found","path":"/datasource/skill/getBySkillId/"} "任务ID": "3320260418002",
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:292] - API调用失败: API请求失败 (HTTP 404): http://192.168.11.24:8088/datasource/skill/getBySkillId/ "原始数据": "{\"name\": \"李四\", \"mobile\": \"13800000102\", \"dept\": \"财务部\", \"amount\": 980}",
Traceback (most recent call last): "处理结果": "导入成功",
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 80, in get_skill_by_id "错误原因": null,
response.raise_for_status() "处理状态": "SUCCESS",
File "D:\anaconda3\Lib\site-packages\httpx\_models.py", line 829, in raise_for_status "关联目标表": "employee_import",
raise HTTPStatusError(message, request=request, response=self) "创建时间": "2026-04-18T15:19:34.000+08:00"
httpx.HTTPStatusError: Client error '404 ' for url 'http://192.168.11.24:8088/datasource/skill/getBySkillId/' },
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404 {
"记录ID": 80003,
During handling of the above exception, another exception occurred: "任务ID": "3320260418003",
"原始数据": "{\"name\": \"王五\", \"mobile\": \"13800000103\", \"dept\": \"技术部\", \"amount\": 1500}",
Traceback (most recent call last): "处理结果": "导入失败",
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 281, in call_third_party_api "错误原因": "字段校验失败mobile格式不正确",
raw_result = get_skill_by_id(skill_id) "处理状态": "FAILED",
^^^^^^^^^^^^^^^^^^^^^^^^^ "关联目标表": "employee_import",
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 208, in get_skill_by_id "创建时间": "2026-04-18T15:19:49.000+08:00"
return default_client.get_skill_by_id(skill_id) },
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ {
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 99, in get_skill_by_id "记录ID": 80004,
raise Exception(error_msg) "任务ID": "3320260418004",
Exception: API请求失败 (HTTP 404): http://192.168.11.24:8088/datasource/skill/getBySkillId/ "原始数据": "{\"name\": \"赵六\", \"mobile\": \"13800000104\", \"dept\": \"人事部\", \"amount\": 0}",
2025-12-31 12:57:29 - mcp_services - WARNING - [main.py:131] - API获取失败降级使用本地配置: API请求失败 (HTTP 404): http://192.168.11.24:8088/datasource/skill/getBySkillId/ "处理结果": "导入成功",
2025-12-31 12:57:29 - mcp_services - INFO - [main.py:55] - 成功加载 3 个业务查询配置 "错误原因": null,
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName' "处理状态": "SUCCESS",
Traceback (most recent call last): "关联目标表": "employee_import",
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query "创建时间": "2026-04-18T15:20:04.000+08:00"
tool_name = query['businessName'] },
~~~~~^^^^^^^^^^^^^^^^ {
KeyError: 'businessName' "记录ID": 80005,
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName' "任务ID": "3320260418005",
Traceback (most recent call last): "原始数据": "{\"name\": \"孙七\", \"mobile\": \"13800000105\", \"dept\": \"运营部\", \"amount\": 2000}",
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools "处理结果": "导入失败",
tool = generate_tool_schema_from_query(query) "错误原因": "目标表写入失败:唯一键冲突",
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "处理状态": "FAILED",
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query "关联目标表": "employee_import",
tool_name = query['businessName'] "创建时间": "2026-04-18T15:20:19.000+08:00"
~~~~~^^^^^^^^^^^^^^^^ },
KeyError: 'businessName' {
2025-12-31 12:57:29 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest "记录ID": 80006,
2025-12-31 12:57:29 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 "任务ID": "3320260418006",
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName' "原始数据": "{\"name\": \"周八\", \"mobile\": \"13800000106\", \"dept\": \"法务部\", \"amount\": 300}",
Traceback (most recent call last): "处理结果": "导入成功",
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query "错误原因": null,
tool_name = query['businessName'] "处理状态": "SUCCESS",
~~~~~^^^^^^^^^^^^^^^^ "关联目标表": "employee_import",
KeyError: 'businessName' "创建时间": "2026-04-18T15:20:34.000+08:00"
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName' }
Traceback (most recent call last): ],
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools "databaseName": "import_log_db_168",
tool = generate_tool_schema_from_query(query) "businessDescription": "qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: 查询数据导入明细表中的前50条记录包含原始数据、处理状态、错误原因等关键信息用于快速预览最近的导入情况。",
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "originalTemplate": "SELECT record_id AS \"记录ID\", task_id AS \"任务ID\", original_data AS \"原始数据\", process_result AS \"处理结果\", error_reason AS \"错误原因\", process_status AS \"处理状态\", target_table AS \"关联目标表\", created_time AS \"创建时间\" FROM import_record_detail LIMIT 50;",
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query "convertedTemplate": "SELECT record_id AS \"记录ID\", task_id AS \"任务ID\", original_data AS \"原始数据\", process_result AS \"处理结果\", error_reason AS \"错误原因\", process_status AS \"处理状态\", target_table AS \"关联目标表\", created_time AS \"创建时间\" FROM import_record_detail LIMIT 50;",
tool_name = query['businessName'] "executionStatus": "success",
~~~~~^^^^^^^^^^^^^^^^ "businessName": "qian50tiaodaorushujumingxichaxun_32b1d628",
KeyError: 'businessName' "testParams": {},
2025-12-31 12:57:54 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs "errorMessage": null,
2025-12-31 12:57:54 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5 "executionTime": 8,
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:347] - 开始运行 MCP SQL Executor 服务器 "datasourceId": "240",
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:299] - ============================================================ "logId": "2058821884796174336",
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:300] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor "executableSql": "SELECT record_id AS \"记录ID\", task_id AS \"任务ID\", original_data AS \"原始数据\", process_result AS \"处理结果\", error_reason AS \"错误原因\", process_status AS \"处理状态\", target_table AS \"关联目标表\", created_time AS \"创建时间\" FROM import_record_detail LIMIT 50;",
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:301] - 版本: 0.1.0 "datasourceName": "import_log_db_168"
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:302] - ============================================================ }
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:306] - 环境配置 - Database ID: 16 }
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:307] - 环境配置 - Skill ID: 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:176] - 响应code: 200
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:308] - 环境配置 - Backend Base URL: http://192.168.11.24:8088 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:177] - 响应msg: 操作成功
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:309] - ============================================================ 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:178] - 响应data: {'resultCount': 6, 'data': [{'记录ID': 80001, '任务ID': '3320260418001', '原始数据': '{"name": "张三", "mobile": "13800000101", "dept": "销售部", "amount": 1200}', '处理结果': '导入成功', '错误原因': None, '处理状态': 'SUCCESS', '关联目标表': 'employee_import', '创建时间': '2026-04-18T15:19:19.000+08:00'}, {'记录ID': 80002, '任务ID': '3320260418002', '原始数据': '{"name": "李四", "mobile": "13800000102", "dept": "财务部", "amount": 980}', '处理结果': '导入成功', '错误原因': None, '处理状态': 'SUCCESS', '关联目标表': 'employee_import', '创建时间': '2026-04-18T15:19:34.000+08:00'}, {'记录ID': 80003, '任务ID': '3320260418003', '原始数据': '{"name": "王五", "mobile": "13800000103", "dept": "技术部", "amount": 1500}', '处理结果': '导入失败', '错误原因': '字段校验失败mobile格式不正确', '处理状态': 'FAILED', '关联目标表': 'employee_import', '创建时间': '2026-04-18T15:19:49.000+08:00'}, {'记录ID': 80004, '任务ID': '3320260418004', '原始数据': '{"name": "赵六", "mobile": "13800000104", "dept": "人事部", "amount": 0}', '处理结果': '导入成功', '错误原因': None, '处理状态': 'SUCCESS', '关联目标表': 'employee_import', '创建时间': '2026-04-18T15:20:04.000+08:00'}, {'记录ID': 80005, '任务ID': '3320260418005', '原始数据': '{"name": "孙七", "mobile": "13800000105", "dept": "运营部", "amount": 2000}', '处理结果': '导入失败', '错误原因': '目标表写入失败:唯一键冲突', '处理状态': 'FAILED', '关联目标表': 'employee_import', '创建时间': '2026-04-18T15:20:19.000+08:00'}, {'记录ID': 80006, '任务ID': '3320260418006', '原始数据': '{"name": "周八", "mobile": "13800000106", "dept": "法务部", "amount": 300}', '处理结果': '导入成功', '错误原因': None, '处理状态': 'SUCCESS', '关联目标表': 'employee_import', '创建时间': '2026-04-18T15:20:34.000+08:00'}], 'databaseName': 'import_log_db_168', 'businessDescription': 'qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: 查询数据导入明细表中的前50条记录包含原始数据、处理状态、错误原因等关键信息用于快速预览最近的导入情况。', 'originalTemplate': 'SELECT record_id AS "记录ID", task_id AS "任务ID", original_data AS "原始数据", process_result AS "处理结果", error_reason AS "错误原因", process_status AS "处理状态", target_table AS "关联目标表", created_time AS "创建时间" FROM import_record_detail LIMIT 50;', 'convertedTemplate': 'SELECT record_id AS "记录ID", task_id AS "任务ID", original_data AS "原始数据", process_result AS "处理结果", error_reason AS "错误原因", process_status AS "处理状态", target_table AS "关联目标表", created_time AS "创建时间" FROM import_record_detail LIMIT 50;', 'executionStatus': 'success', 'businessName': 'qian50tiaodaorushujumingxichaxun_32b1d628', 'testParams': {}, 'errorMessage': None, 'executionTime': 8, 'datasourceId': '240', 'logId': '2058821884796174336', 'executableSql': 'SELECT record_id AS "记录ID", task_id AS "任务ID", original_data AS "原始数据", process_result AS "处理结果", error_reason AS "错误原因", process_status AS "处理状态", target_table AS "关联目标表", created_time AS "创建时间" FROM import_record_detail LIMIT 50;', 'datasourceName': 'import_log_db_168'}
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:314] - MCP 服务器已启动,等待客户端连接... 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:179] - ================================================================================
2025-12-31 12:57:54 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest 2026-05-25 16:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:181] - 测试SQL API调用成功
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 2026-05-25 16:05:52 - mcp_services - INFO - [main.py:232] - 测试SQL API调用成功
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local... 2026-05-25 16:15:08 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:55] - 成功加载 3 个业务查询配置 2026-05-25 16:15:08 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:123] - 本地配置: 3 条 2026-05-25 16:15:08 - mcp_services - INFO - [main.py:246] - 开始运行 MCP SQL Executor 服务
2025-12-31 12:57:54 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName' 2026-05-25 16:15:08 - mcp_services - INFO - [main.py:204] - ============================================================
Traceback (most recent call last): 2026-05-25 16:15:08 - mcp_services - INFO - [main.py:205] - 正在启动 MCP 服务: lzwcai-mcp-sqlexecutor
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query 2026-05-25 16:15:08 - mcp_services - INFO - [main.py:206] - 版本: 0.1.0
tool_name = query['businessName'] 2026-05-25 16:15:08 - mcp_services - INFO - [main.py:207] - ============================================================
~~~~~^^^^^^^^^^^^^^^^ 2026-05-25 16:15:08 - mcp_services - INFO - [main.py:210] - 环境配置 - Database ID: 240
KeyError: 'businessName' 2026-05-25 16:15:08 - mcp_services - INFO - [main.py:211] - 环境配置 - Skill ID: 2058819964077572098
2025-12-31 12:57:54 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName' 2026-05-25 16:15:08 - mcp_services - INFO - [main.py:212] - 环境配置 - Backend Base URL: http://192.168.2.236:8088
Traceback (most recent call last): 2026-05-25 16:15:08 - mcp_services - INFO - [main.py:213] - ============================================================
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools 2026-05-25 16:15:08 - mcp_services - INFO - [main.py:218] - MCP 服务已启动,等待客户端连接...
tool = generate_tool_schema_from_query(query) 2026-05-25 16:15:09 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2026-05-25 16:15:09 - mcp_services - INFO - [main.py:107] - 收到列出工具请求
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query 2026-05-25 16:15:09 - mcp_services - INFO - [main.py:77] - 初始化查询配置,数据源: api
tool_name = query['businessName'] 2026-05-25 16:15:09 - mcp_services - INFO - [main.py:189] - 调用第三方 APIskill_id: 2058819964077572098
~~~~~^^^^^^^^^^^^^^^^ 2026-05-25 16:15:09 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://192.168.2.236:8088/datasource/skill/getBySkillId/2058819964077572098
KeyError: 'businessName' 2026-05-25 16:15:09 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/datasource/skill/getBySkillId/2058819964077572098 "HTTP/1.1 200 "
2025-12-31 15:00:31 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest 2026-05-25 16:15:09 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:85] - API调用成功: http://192.168.2.236:8088/datasource/skill/getBySkillId/2058819964077572098
2025-12-31 15:00:31 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 2026-05-25 16:15:09 - mcp_services - INFO - [main.py:191] - 成功获取原始响应: {'msg': '查询成功', 'code': 200, 'data': [{'id': '2058819964287287298', 'createBy': 'wxl06', 'createTime': '2026-05-25 15:58:25', 'updateBy': None, 'updateTime': None, 'serviceId': '2058819964085960705', 'uniqueName': '前50条导入数据明细查询', 'name': 'qian50tiaodaorushujumingxichaxun_32b1d628', 'description': 'qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: 查询数据导入明细表中的前50条记录包含原始数据、处理状态、错误原因等关键信息用于快速预览最近的导入情况。', 'visualizable': 1, 'toolPrompt': '查询成功,返回 6 行数据,执行时间: 1ms', 'toolType': 'sql', 'datasourceId': '240', 'sqlTemplate': 'SELECT record_id AS "记录ID", task_id AS "任务ID", original_data AS "原始数据", process_result AS "处理结果", error_reason AS "错误原因", process_status AS "处理状态", target_table AS "关联目标表", created_time AS "创建时间" FROM import_record_detail LIMIT 50;', 'sqlParams': '{}', 'resultType': 'list', 'sourceType': 'ai', 'trainingTaskId': None, 'tableMetadataIds': '', 'executionCount': 0, 'visualizationConfigs': None, 'inputJsonSchema': '{"type":"object","properties":{},"required":[]}', 'outputJsonSchema': '{"type":"object","properties":{"text":{"type":"string"}},"additionalProperties":false}', 'lastExecutionTime': None}]}
2025-12-31 15:00:31 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName' 2026-05-25 16:15:09 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:325] - 成功处理 1 条技能数据
Traceback (most recent call last): 2026-05-25 16:15:09 - mcp_services - INFO - [main.py:194] - 成功处理 1 条数据
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query 2026-05-25 16:15:09 - mcp_services - INFO - [main.py:85] - API 配置: 1 条
tool_name = query['businessName'] 2026-05-25 16:15:09 - mcp_services - INFO - [main.py:86] - API 配置数组: [{'id': '2058819964287287298', 'businessName': 'qian50tiaodaorushujumingxichaxun_32b1d628', 'businessDescription': 'qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: 查询数据导入明细表中的前50条记录包含原始数据、处理状态、错误原因等关键信息用于快速预览最近的导入情况。', 'sqlTemplate': 'SELECT record_id AS "记录ID", task_id AS "任务ID", original_data AS "原始数据", process_result AS "处理结果", error_reason AS "错误原因", process_status AS "处理状态", target_table AS "关联目标表", created_time AS "创建时间" FROM import_record_detail LIMIT 50;', 'parameters': {}, 'datasourceId': '240'}]
~~~~~^^^^^^^^^^^^^^^^ 2026-05-25 16:15:09 - mcp_services - INFO - [main.py:110] - 成功生成 1 个 MCP 工具
KeyError: 'businessName' 2026-05-25 16:22:48 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs
2025-12-31 15:00:31 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName' 2026-05-25 16:22:48 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5
Traceback (most recent call last): 2026-05-25 16:22:48 - mcp_services - INFO - [main.py:258] - 开始运行 MCP SQL Executor 服务
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools 2026-05-25 16:22:48 - mcp_services - INFO - [main.py:216] - ============================================================
tool = generate_tool_schema_from_query(query) 2026-05-25 16:22:48 - mcp_services - INFO - [main.py:217] - 正在启动 MCP 服务: lzwcai-mcp-sqlexecutor
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2026-05-25 16:22:48 - mcp_services - INFO - [main.py:218] - 版本: 0.1.0
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query 2026-05-25 16:22:48 - mcp_services - INFO - [main.py:219] - ============================================================
tool_name = query['businessName'] 2026-05-25 16:22:48 - mcp_services - INFO - [main.py:222] - 环境配置 - Database ID: 29
~~~~~^^^^^^^^^^^^^^^^ 2026-05-25 16:22:48 - mcp_services - INFO - [main.py:223] - 环境配置 - Skill ID:
KeyError: 'businessName' 2026-05-25 16:22:48 - mcp_services - INFO - [main.py:224] - 环境配置 - Backend Base URL: http://lzwcai-demp-corp-manager:8086
2025-12-31 15:00:34 - mcp_services - INFO - [main.py:329] - MCP 服务器已关闭 2026-05-25 16:22:48 - mcp_services - INFO - [main.py:225] - ============================================================
2025-12-31 15:00:53 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs 2026-05-25 16:22:48 - mcp_services - INFO - [main.py:230] - MCP 服务已启动,等待客户端连接...
2025-12-31 15:00:53 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5 2026-05-25 16:22:51 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:347] - 开始运行 MCP SQL Executor 服务器 2026-05-25 16:22:51 - mcp_services - INFO - [main.py:127] - 收到列出工具请求
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:299] - ============================================================ 2026-05-25 16:22:51 - mcp_services - INFO - [main.py:97] - 初始化查询配置,数据源: api
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:300] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor 2026-05-25 16:22:51 - mcp_services - INFO - [main.py:201] - 调用第三方 APIskill_id:
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:301] - 版本: 0.1.0 2026-05-25 16:22:51 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:47] - 正在调用 API: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:302] - ============================================================ 2026-05-25 16:22:53 - lzwcai_mcp_sqlexecutor.utils.api_client - ERROR - [api_client.py:69] - API 请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:306] - 环境配置 - Database ID: 29 2026-05-25 16:22:53 - mcp_services - ERROR - [main.py:209] - API 调用失败: API 请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:307] - 环境配置 - Skill ID:
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:308] - 环境配置 - Backend Base URL: http://lzwcai-demp-corp-manager:8086
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:309] - ============================================================
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:314] - MCP 服务器已启动,等待客户端连接...
2025-12-31 15:00:54 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest
2025-12-31 15:00:54 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
2025-12-31 15:00:54 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api...
2025-12-31 15:00:54 - mcp_services - INFO - [main.py:278] - 调用第三方APIskill_id:
2025-12-31 15:00:54 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/
2025-12-31 15:00:56 - lzwcai_mcp_sqlexecutor.utils.api_client - ERROR - [api_client.py:103] - API请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
2025-12-31 15:00:56 - mcp_services - ERROR - [main.py:292] - API调用失败: API请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
Traceback (most recent call last): Traceback (most recent call last):
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 101, in map_httpcore_exceptions File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 101, in map_httpcore_exceptions
yield yield
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 250, in handle_request File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 250, in handle_request
resp = self._pool.handle_request(req) resp = self._pool.handle_request(req)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection_pool.py", line 216, in handle_request File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection_pool.py", line 256, in handle_request
raise exc from None raise exc from None
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection_pool.py", line 196, in handle_request File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection_pool.py", line 236, in handle_request
response = connection.handle_request( response = connection.handle_request(
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 99, in handle_request File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 101, in handle_request
raise exc raise exc
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 76, in handle_request File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 78, in handle_request
stream = self._connect(request) stream = self._connect(request)
^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 122, in _connect File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 124, in _connect
stream = self._network_backend.connect_tcp(**kwargs) stream = self._network_backend.connect_tcp(**kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\anaconda3\Lib\site-packages\httpcore\_backends\sync.py", line 205, in connect_tcp File "D:\anaconda3\Lib\site-packages\httpcore\_backends\sync.py", line 207, in connect_tcp
with map_exceptions(exc_map): with map_exceptions(exc_map):
^^^^^^^^^^^^^^^^^^^^^^^
File "D:\anaconda3\Lib\contextlib.py", line 158, in __exit__ File "D:\anaconda3\Lib\contextlib.py", line 158, in __exit__
self.gen.throw(value) self.gen.throw(value)
File "D:\anaconda3\Lib\site-packages\httpcore\_exceptions.py", line 14, in map_exceptions File "D:\anaconda3\Lib\site-packages\httpcore\_exceptions.py", line 14, in map_exceptions
@@ -224,9 +212,9 @@ httpcore.ConnectError: [Errno 11001] getaddrinfo failed
The above exception was the direct cause of the following exception: The above exception was the direct cause of the following exception:
Traceback (most recent call last): Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 74, in get_skill_by_id File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 50, in get_skill_by_id
response = self.client.get( response = self.client.get(url, headers=self._get_headers())
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\anaconda3\Lib\site-packages\httpx\_client.py", line 1053, in get File "D:\anaconda3\Lib\site-packages\httpx\_client.py", line 1053, in get
return self.request( return self.request(
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
@@ -247,7 +235,6 @@ Traceback (most recent call last):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 249, in handle_request File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 249, in handle_request
with map_httpcore_exceptions(): with map_httpcore_exceptions():
^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\anaconda3\Lib\contextlib.py", line 158, in __exit__ File "D:\anaconda3\Lib\contextlib.py", line 158, in __exit__
self.gen.throw(value) self.gen.throw(value)
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 118, in map_httpcore_exceptions File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 118, in map_httpcore_exceptions
@@ -257,18 +244,46 @@ httpx.ConnectError: [Errno 11001] getaddrinfo failed
During handling of the above exception, another exception occurred: During handling of the above exception, another exception occurred:
Traceback (most recent call last): Traceback (most recent call last):
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 281, in call_third_party_api File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 202, in call_third_party_api
raw_result = get_skill_by_id(skill_id) raw_result = get_skill_by_id(skill_id)
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 208, in get_skill_by_id File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 154, in get_skill_by_id
return default_client.get_skill_by_id(skill_id) return default_client.get_skill_by_id(skill_id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 104, in get_skill_by_id File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 70, in get_skill_by_id
raise Exception(error_msg) raise Exception(error_msg)
Exception: API 请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed Exception: API 请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
2025-12-31 15:00:56 - mcp_services - WARNING - [main.py:131] - API获取失败降级使用本地配置: API请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed 2026-05-25 16:22:53 - mcp_services - WARNING - [main.py:108] - API 获取失败,降级使用本地配置: API 请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
2025-12-31 15:00:56 - mcp_services - INFO - [main.py:55] - 成功加载 0 个业务查询配置 2026-05-25 16:22:53 - mcp_services - INFO - [main.py:58] - 成功加载 0 个业务查询配置
2025-12-31 15:00:56 - mcp_services - INFO - [main.py:165] - 成功生成 0 个 MCP 工具 2026-05-25 16:22:53 - mcp_services - INFO - [main.py:130] - 成功生成 0 个 MCP 工具
2025-12-31 15:00:56 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest 2026-05-25 16:22:53 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2025-12-31 15:00:56 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 2026-05-25 16:22:53 - mcp_services - INFO - [main.py:127] - 收到列出工具请求
2025-12-31 15:00:56 - mcp_services - INFO - [main.py:165] - 成功生成 0 个 MCP 工具 2026-05-25 16:22:53 - mcp_services - INFO - [main.py:130] - 成功生成 0 个 MCP 工具
2026-05-25 16:23:09 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-05-25 16:23:09 - mcp_services - INFO - [main.py:127] - 收到列出工具请求
2026-05-25 16:23:09 - mcp_services - INFO - [main.py:130] - 成功生成 0 个 MCP 工具
2026-05-25 16:26:37 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs
2026-05-25 16:26:37 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5
2026-05-25 16:26:37 - mcp_services - INFO - [main.py:258] - 开始运行 MCP SQL Executor 服务
2026-05-25 16:26:37 - mcp_services - INFO - [main.py:216] - ============================================================
2026-05-25 16:26:37 - mcp_services - INFO - [main.py:217] - 正在启动 MCP 服务: lzwcai-mcp-sqlexecutor
2026-05-25 16:26:37 - mcp_services - INFO - [main.py:218] - 版本: 0.1.0
2026-05-25 16:26:37 - mcp_services - INFO - [main.py:219] - ============================================================
2026-05-25 16:26:37 - mcp_services - INFO - [main.py:222] - 环境配置 - Database ID: 240
2026-05-25 16:26:37 - mcp_services - INFO - [main.py:223] - 环境配置 - Skill ID: 2058819964077572098
2026-05-25 16:26:37 - mcp_services - INFO - [main.py:224] - 环境配置 - Backend Base URL: http://192.168.2.236:8088
2026-05-25 16:26:37 - mcp_services - INFO - [main.py:225] - ============================================================
2026-05-25 16:26:37 - mcp_services - INFO - [main.py:230] - MCP 服务已启动,等待客户端连接...
2026-05-25 16:26:38 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-05-25 16:26:38 - mcp_services - INFO - [main.py:127] - 收到列出工具请求
2026-05-25 16:26:38 - mcp_services - INFO - [main.py:97] - 初始化查询配置,数据源: api
2026-05-25 16:26:38 - mcp_services - INFO - [main.py:201] - 调用第三方 APIskill_id: 2058819964077572098
2026-05-25 16:26:38 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:47] - 正在调用 API: http://192.168.2.236:8088/datasource/skill/getBySkillId/2058819964077572098
2026-05-25 16:26:38 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/datasource/skill/getBySkillId/2058819964077572098 "HTTP/1.1 200 "
2026-05-25 16:26:38 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:54] - API 调用成功: http://192.168.2.236:8088/datasource/skill/getBySkillId/2058819964077572098
2026-05-25 16:26:38 - mcp_services - INFO - [main.py:203] - 成功获取原始响应: {'msg': '查询成功', 'code': 200, 'data': [{'id': '2058819964287287298', 'createBy': 'wxl06', 'createTime': '2026-05-25 15:58:25', 'updateBy': None, 'updateTime': None, 'serviceId': '2058819964085960705', 'uniqueName': '前50条导入数据明细查询', 'name': 'qian50tiaodaorushujumingxichaxun_32b1d628', 'description': 'qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: 查询数据导入明细表中的前50条记录包含原始数据、处理状态、错误原因等关键信息用于快速预览最近的导入情况。', 'visualizable': 1, 'toolPrompt': '查询成功,返回 6 行数据,执行时间: 1ms', 'toolType': 'sql', 'datasourceId': '240', 'sqlTemplate': 'SELECT record_id AS "记录ID", task_id AS "任务ID", original_data AS "原始数据", process_result AS "处理结果", error_reason AS "错误原因", process_status AS "处理状态", target_table AS "关联目标表", created_time AS "创建时间" FROM import_record_detail LIMIT 50;', 'sqlParams': '{}', 'resultType': 'list', 'sourceType': 'ai', 'trainingTaskId': None, 'tableMetadataIds': '', 'executionCount': 0, 'visualizationConfigs': None, 'inputJsonSchema': '{"type":"object","properties":{},"required":[]}', 'outputJsonSchema': '{"type":"object","properties":{"text":{"type":"string"}},"additionalProperties":false}', 'lastExecutionTime': None}]}
2026-05-25 16:26:38 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:205] - 成功处理 1 条技能数据
2026-05-25 16:26:38 - mcp_services - INFO - [main.py:206] - 成功处理 1 条数据
2026-05-25 16:26:38 - mcp_services - INFO - [main.py:105] - API 配置: 1 条
2026-05-25 16:26:38 - mcp_services - INFO - [main.py:106] - API 配置数组: [{'id': '2058819964287287298', 'businessName': 'qian50tiaodaorushujumingxichaxun_32b1d628', 'businessDescription': 'qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: qian50tiaodaorushujumingxichaxun_32b1d628: 查询数据导入明细表中的前50条记录包含原始数据、处理状态、错误原因等关键信息用于快速预览最近的导入情况。', 'sqlTemplate': 'SELECT record_id AS "记录ID", task_id AS "任务ID", original_data AS "原始数据", process_result AS "处理结果", error_reason AS "错误原因", process_status AS "处理状态", target_table AS "关联目标表", created_time AS "创建时间" FROM import_record_detail LIMIT 50;', 'parameters': {}, 'datasourceId': '240'}]
2026-05-25 16:26:38 - mcp_services - INFO - [main.py:130] - 成功生成 1 个 MCP 工具

Some files were not shown because too many files have changed in this diff Show More