cq-mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -0
- package/dist/index.js +279 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# CloudQuery MCP Server 使用指南
|
|
2
|
+
|
|
3
|
+
本 MCP Server 连接 CloudQuery 平台,提供四个工具,让 AI 能够探索数据库结构并执行 SQL 查询。
|
|
4
|
+
|
|
5
|
+
## 工具调用顺序
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
list_databases → list_tables → get_table_columns → execute_sql
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**必须按顺序调用**:后续工具所需的 `connection_id`、`connection_type`、`database`、`schema` 参数,都只能从前面的工具返回结果中获取,不能凭空猜测。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 工具详解
|
|
16
|
+
|
|
17
|
+
### 1. `list_databases` — 查询数据库清单
|
|
18
|
+
|
|
19
|
+
**无需任何参数。**
|
|
20
|
+
|
|
21
|
+
返回所有数据库连接及其下的数据库列表,格式如下:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
## 连接: pg-prod (connection_id=1, connection_type=PostgreSQL)
|
|
25
|
+
- pam
|
|
26
|
+
- postgres
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**从返回结果中记录:**
|
|
30
|
+
- `connection_id`(数字)
|
|
31
|
+
- `connection_type`(字符串,如 `PostgreSQL`)
|
|
32
|
+
- 目标数据库名称(如 `pam`)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### 2. `list_tables` — 查询表清单
|
|
37
|
+
|
|
38
|
+
**参数(全部必填):**
|
|
39
|
+
|
|
40
|
+
| 参数 | 来源 | 说明 |
|
|
41
|
+
|------|------|------|
|
|
42
|
+
| `connection_id` | list_databases 返回 | 数字 |
|
|
43
|
+
| `connection_type` | list_databases 返回 | 如 `PostgreSQL` |
|
|
44
|
+
| `database` | list_databases 返回 | 数据库名,如 `pam` |
|
|
45
|
+
| `schema` | 推断或尝试 | PostgreSQL 常用 `public` 或与库同名(如 `pam`);若 `public` 无结果,改用库名 |
|
|
46
|
+
|
|
47
|
+
返回该 schema 下所有表名列表。
|
|
48
|
+
|
|
49
|
+
**从返回结果中记录:** 目标表名(如 `auth_role`)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### 3. `get_table_columns` — 查询表字段
|
|
54
|
+
|
|
55
|
+
**参数(全部必填):**
|
|
56
|
+
|
|
57
|
+
| 参数 | 来源 |
|
|
58
|
+
|------|------|
|
|
59
|
+
| `connection_id` | list_databases |
|
|
60
|
+
| `connection_type` | list_databases |
|
|
61
|
+
| `database` | list_databases |
|
|
62
|
+
| `schema` | list_tables 调用时使用的 schema |
|
|
63
|
+
| `table` | list_tables 返回 |
|
|
64
|
+
|
|
65
|
+
返回字段名、数据类型、长度、是否可空、业务注释,格式:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
字段名 类型 长度 可空 注释
|
|
69
|
+
---------------------------------------------------------------------------
|
|
70
|
+
id int8 19 否 主键
|
|
71
|
+
role_name varchar 64 否 角色名称
|
|
72
|
+
role_code varchar 32 否 角色编码
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**从返回结果中记录:** 字段名列表,用于编写 SQL。
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### 4. `execute_sql` — 执行 SQL
|
|
80
|
+
|
|
81
|
+
**参数:**
|
|
82
|
+
|
|
83
|
+
| 参数 | 来源 | 说明 |
|
|
84
|
+
|------|------|------|
|
|
85
|
+
| `connection_id` | list_databases | |
|
|
86
|
+
| `connection_type` | list_databases | 默认 `PostgreSQL` |
|
|
87
|
+
| `database` | list_databases | |
|
|
88
|
+
| `schema` | 同 list_tables | 作为 SQL 的 search_path |
|
|
89
|
+
| `sql` | 根据字段结构编写 | 一次只执行一条语句 |
|
|
90
|
+
|
|
91
|
+
**SELECT 返回格式:**
|
|
92
|
+
```
|
|
93
|
+
id | role_name | role_code | description
|
|
94
|
+
----------------------------------------
|
|
95
|
+
1 | 超级管理员 | SUPER_ADMIN | 系统最高权限
|
|
96
|
+
2 | 普通用户 | USER | 默认角色
|
|
97
|
+
|
|
98
|
+
共 8 行 耗时 12ms
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**DML 返回格式:**
|
|
102
|
+
```
|
|
103
|
+
执行成功
|
|
104
|
+
影响行数: 1
|
|
105
|
+
耗时: 5ms
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 典型场景:查询角色表数据
|
|
111
|
+
|
|
112
|
+
以下是完整的 6 步流程示例:
|
|
113
|
+
|
|
114
|
+
**Step 1** — 调用 `list_databases`(无参数)
|
|
115
|
+
- 获得:`connection_id=1, connection_type=PostgreSQL`,数据库列表包含 `pam`
|
|
116
|
+
|
|
117
|
+
**Step 2** — 调用 `list_tables`
|
|
118
|
+
```json
|
|
119
|
+
{ "connection_id": 1, "connection_type": "PostgreSQL", "database": "pam", "schema": "pam" }
|
|
120
|
+
```
|
|
121
|
+
- 获得:表列表,找到 `auth_role`
|
|
122
|
+
|
|
123
|
+
**Step 3** — 调用 `get_table_columns`
|
|
124
|
+
```json
|
|
125
|
+
{ "connection_id": 1, "connection_type": "PostgreSQL", "database": "pam", "schema": "pam", "table": "auth_role" }
|
|
126
|
+
```
|
|
127
|
+
- 获得:字段列表 `id, role_name, role_code, description, ...`
|
|
128
|
+
|
|
129
|
+
**Step 4** — 根据字段编写 SQL:
|
|
130
|
+
```sql
|
|
131
|
+
SELECT id, role_name, role_code, description FROM pam.auth_role LIMIT 20
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Step 5** — 调用 `execute_sql`
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"connection_id": 1,
|
|
138
|
+
"connection_type": "PostgreSQL",
|
|
139
|
+
"database": "pam",
|
|
140
|
+
"schema": "pam",
|
|
141
|
+
"sql": "SELECT id, role_name, role_code, description FROM pam.auth_role LIMIT 20"
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
- 获得:查询结果
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 注意事项
|
|
149
|
+
|
|
150
|
+
1. **schema 选择**:PostgreSQL 数据库的 schema 名通常与数据库同名(如数据库 `pam` 对应 schema `pam`),而不是 `public`。若 `public` 无结果,改用数据库名作为 schema。
|
|
151
|
+
|
|
152
|
+
2. **单条执行**:`execute_sql` 每次只能执行一条 SQL 语句,不支持批量执行。
|
|
153
|
+
|
|
154
|
+
3. **结果上限**:SELECT 查询最多返回 500 行。
|
|
155
|
+
|
|
156
|
+
4. **SQL 写法**:建议在表名前加 schema 前缀(如 `pam.auth_role`),避免歧义。
|
|
157
|
+
|
|
158
|
+
5. **参数不能猜测**:`connection_id` 和 `connection_type` 必须通过 `list_databases` 获取,不能假设固定值。
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import forge from "node-forge";
|
|
6
|
+
const BASE_URL = process.env.CQ_BASE_URL ?? "http://10.10.2.73";
|
|
7
|
+
const USERNAME = process.env.CQ_USERNAME ?? "admin001";
|
|
8
|
+
const PASSWORD = process.env.CQ_PASSWORD ?? "Hello123$";
|
|
9
|
+
// MCP 协议通过 stdio 通信,日志必须输出到 stderr 避免污染 stdout
|
|
10
|
+
const log = {
|
|
11
|
+
info: (...args) => process.stderr.write(`[INFO] ${args.join(" ")}\n`),
|
|
12
|
+
warn: (...args) => process.stderr.write(`[WARN] ${args.join(" ")}\n`),
|
|
13
|
+
error: (...args) => process.stderr.write(`[ERROR] ${args.join(" ")}\n`),
|
|
14
|
+
};
|
|
15
|
+
let sessionCookie = null;
|
|
16
|
+
// ── 认证 ──────────────────────────────────────────────────────────
|
|
17
|
+
/** 用 RSA 公钥加密密码(与前端 JSEncrypt 行为一致) */
|
|
18
|
+
function encryptPassword(publicKeyB64, password) {
|
|
19
|
+
const derBytes = forge.util.decode64(publicKeyB64);
|
|
20
|
+
const asn1 = forge.asn1.fromDer(derBytes);
|
|
21
|
+
const publicKey = forge.pki.publicKeyFromAsn1(asn1);
|
|
22
|
+
const encrypted = publicKey.encrypt(password, "RSAES-PKCS1-V1_5");
|
|
23
|
+
return forge.util.encode64(encrypted);
|
|
24
|
+
}
|
|
25
|
+
async function login() {
|
|
26
|
+
log.info(`登录 ${BASE_URL},用户: ${USERNAME}`);
|
|
27
|
+
const keyResp = await fetch(`${BASE_URL}/user/sys/transmission/publicKey`);
|
|
28
|
+
if (!keyResp.ok)
|
|
29
|
+
throw new Error(`获取公钥失败: HTTP ${keyResp.status}`);
|
|
30
|
+
const keyData = (await keyResp.json());
|
|
31
|
+
if (keyData.resCode !== 10000)
|
|
32
|
+
throw new Error(`获取公钥异常: ${keyData.resCode}`);
|
|
33
|
+
const encryptedPassword = encryptPassword(keyData.data.publicKey, PASSWORD);
|
|
34
|
+
const resp = await fetch(`${BASE_URL}/user/login`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ userId: USERNAME, password: encryptedPassword }),
|
|
38
|
+
});
|
|
39
|
+
if (!resp.ok)
|
|
40
|
+
throw new Error(`登录请求失败: HTTP ${resp.status}`);
|
|
41
|
+
const data = (await resp.json());
|
|
42
|
+
if (data.resCode !== 10000)
|
|
43
|
+
throw new Error(`登录失败: ${data.resMsg}`);
|
|
44
|
+
// SESSION cookie = base64(sessionId)
|
|
45
|
+
const sid = data.data?.sessionId;
|
|
46
|
+
if (sid) {
|
|
47
|
+
sessionCookie = Buffer.from(sid).toString("base64");
|
|
48
|
+
log.info("登录成功");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const setCookie = resp.headers.get("set-cookie") ?? "";
|
|
52
|
+
const match = setCookie.match(/SESSION=([^;]+)/);
|
|
53
|
+
if (match) {
|
|
54
|
+
sessionCookie = match[1];
|
|
55
|
+
log.info("登录成功(cookie)");
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
throw new Error("登录成功但未获取到 SESSION");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// ── HTTP 封装 ─────────────────────────────────────────────────────
|
|
62
|
+
async function post(path, body) {
|
|
63
|
+
if (!sessionCookie)
|
|
64
|
+
await login();
|
|
65
|
+
const resp = await fetch(`${BASE_URL}${path}`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
Cookie: `SESSION=${sessionCookie}`,
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify(body),
|
|
72
|
+
});
|
|
73
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
74
|
+
log.warn("Session 已过期,重新登录...");
|
|
75
|
+
sessionCookie = null;
|
|
76
|
+
await login();
|
|
77
|
+
return post(path, body);
|
|
78
|
+
}
|
|
79
|
+
if (!resp.ok)
|
|
80
|
+
throw new Error(`HTTP ${resp.status} ${path}`);
|
|
81
|
+
const result = (await resp.json());
|
|
82
|
+
if (result.resCode !== 10000)
|
|
83
|
+
throw new Error(`[${result.resCode}] ${result.resMsg}`);
|
|
84
|
+
return result.data;
|
|
85
|
+
}
|
|
86
|
+
async function metaNode(payload) {
|
|
87
|
+
const data = await post("/dms/meta/node", {
|
|
88
|
+
...payload,
|
|
89
|
+
globalHavePermissionFlag: false,
|
|
90
|
+
});
|
|
91
|
+
return data?.data ?? [];
|
|
92
|
+
}
|
|
93
|
+
// ── MCP Server ────────────────────────────────────────────────────
|
|
94
|
+
const server = new McpServer({
|
|
95
|
+
name: "cq-mcp-server",
|
|
96
|
+
version: "0.1.0",
|
|
97
|
+
});
|
|
98
|
+
// ── 工具一:查询数据库清单 ─────────────────────────────────────────
|
|
99
|
+
server.tool("list_databases", `查询所有数据库连接及其下的数据库实例列表。
|
|
100
|
+
|
|
101
|
+
用途:了解当前 CloudQuery 平台上有哪些数据库连接,以及每个连接下有哪些数据库。
|
|
102
|
+
返回:连接名称、连接ID、数据库类型,以及每个连接下的数据库列表。
|
|
103
|
+
后续:拿到 connection_id 和 connection_type 后,可调用 list_tables 查看具体库的表。`, {}, async () => {
|
|
104
|
+
log.info("list_databases: 查询连接列表");
|
|
105
|
+
const connections = await metaNode({ nodeType: "root", nodePath: "/root" });
|
|
106
|
+
if (!connections.length)
|
|
107
|
+
return { content: [{ type: "text", text: "未找到任何数据库连接" }] };
|
|
108
|
+
const lines = [];
|
|
109
|
+
for (const conn of connections) {
|
|
110
|
+
const { connectionId, connectionType, nodeName } = conn;
|
|
111
|
+
lines.push(`## 连接: ${nodeName} (connection_id=${connectionId}, connection_type=${connectionType})`);
|
|
112
|
+
const databases = await metaNode({
|
|
113
|
+
nodeType: "connection",
|
|
114
|
+
nodePath: `/root/${connectionId}`,
|
|
115
|
+
nodePathWithType: `/CONNECTION:${connectionId}`,
|
|
116
|
+
connectionId,
|
|
117
|
+
connectionType,
|
|
118
|
+
});
|
|
119
|
+
log.info(` └─ ${nodeName}: ${databases.length} 个数据库`);
|
|
120
|
+
if (databases.length) {
|
|
121
|
+
for (const db of databases)
|
|
122
|
+
lines.push(` - ${db.nodeName}`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
lines.push(" (无数据库)");
|
|
126
|
+
}
|
|
127
|
+
lines.push("");
|
|
128
|
+
}
|
|
129
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
130
|
+
});
|
|
131
|
+
// ── 工具二:查询表清单 ────────────────────────────────────────────
|
|
132
|
+
server.tool("list_tables", `查询指定数据库 Schema 下的所有普通表。
|
|
133
|
+
|
|
134
|
+
用途:了解某个数据库 Schema 下有哪些表,为后续字段查询或 SQL 编写做准备。
|
|
135
|
+
参数说明:
|
|
136
|
+
- connection_id / connection_type:从 list_databases 获取
|
|
137
|
+
- database:数据库名称(如 pam、postgres)
|
|
138
|
+
- schema:Schema 名称,PostgreSQL 常用 public 或与库同名的 schema(如 pam)
|
|
139
|
+
返回:表名列表及总数。
|
|
140
|
+
后续:用 get_table_columns 查看具体表的字段结构。`, {
|
|
141
|
+
connection_id: z.number().describe("数据库连接ID,从 list_databases 获取"),
|
|
142
|
+
connection_type: z.string().describe("数据库类型,如 PostgreSQL、MySQL,从 list_databases 获取"),
|
|
143
|
+
database: z.string().describe("数据库名称,如 pam、postgres"),
|
|
144
|
+
schema: z.string().default("public").describe("Schema 名称,PostgreSQL 默认 public"),
|
|
145
|
+
}, async ({ connection_id, connection_type, database, schema }) => {
|
|
146
|
+
log.info(`list_tables: ${database}.${schema} (conn=${connection_id})`);
|
|
147
|
+
const tables = await metaNode({
|
|
148
|
+
nodeType: "tableGroup",
|
|
149
|
+
nodePath: `/root/${connection_id}/${database}/${schema}/tables`,
|
|
150
|
+
nodePathWithType: `/CONNECTION:${connection_id}/DATABASE:${database}/SCHEMA:${schema}`,
|
|
151
|
+
connectionId: connection_id,
|
|
152
|
+
connectionType: connection_type,
|
|
153
|
+
});
|
|
154
|
+
if (!tables.length) {
|
|
155
|
+
return { content: [{ type: "text", text: `${database}.${schema} 下未找到任何表` }] };
|
|
156
|
+
}
|
|
157
|
+
log.info(` └─ 共 ${tables.length} 张表`);
|
|
158
|
+
const lines = [`${database}.${schema} 共 ${tables.length} 张表:`, ""];
|
|
159
|
+
for (const t of tables)
|
|
160
|
+
lines.push(`- ${t.nodeName}`);
|
|
161
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
162
|
+
});
|
|
163
|
+
// ── 工具三:查询表字段 ────────────────────────────────────────────
|
|
164
|
+
server.tool("get_table_columns", `查询指定表的完整字段定义,包括字段名、数据类型、长度、是否可空、业务注释。
|
|
165
|
+
|
|
166
|
+
用途:了解表结构,辅助编写精确的 SQL 查询语句。
|
|
167
|
+
参数说明:
|
|
168
|
+
- connection_id / connection_type:从 list_databases 获取
|
|
169
|
+
- database / schema:从 list_databases 获取
|
|
170
|
+
- table:从 list_tables 获取
|
|
171
|
+
返回:字段名、数据类型(如 int8/varchar/timestamp)、长度、是否可空(是/否)、字段注释。`, {
|
|
172
|
+
connection_id: z.number().describe("数据库连接ID,从 list_databases 获取"),
|
|
173
|
+
connection_type: z.string().describe("数据库类型,如 PostgreSQL、MySQL"),
|
|
174
|
+
database: z.string().describe("数据库名称"),
|
|
175
|
+
schema: z.string().describe("Schema 名称"),
|
|
176
|
+
table: z.string().describe("表名,从 list_tables 获取"),
|
|
177
|
+
}, async ({ connection_id, connection_type, database, schema, table }) => {
|
|
178
|
+
log.info(`get_table_columns: ${database}.${schema}.${table} (conn=${connection_id})`);
|
|
179
|
+
const columns = await metaNode({
|
|
180
|
+
nodeType: "columnGroup",
|
|
181
|
+
nodePath: `/root/${connection_id}/${database}/${schema}/tables/${table}/columns`,
|
|
182
|
+
nodePathWithType: `/CONNECTION:${connection_id}/DATABASE:${database}/SCHEMA:${schema}/TABLE:${table}`,
|
|
183
|
+
connectionId: connection_id,
|
|
184
|
+
connectionType: connection_type,
|
|
185
|
+
});
|
|
186
|
+
if (!columns.length) {
|
|
187
|
+
return { content: [{ type: "text", text: `表 ${database}.${schema}.${table} 未找到字段信息` }] };
|
|
188
|
+
}
|
|
189
|
+
log.info(` └─ ${columns.length} 个字段`);
|
|
190
|
+
const lines = [
|
|
191
|
+
`表 ${database}.${schema}.${table} 共 ${columns.length} 个字段:`,
|
|
192
|
+
"",
|
|
193
|
+
`${"字段名".padEnd(25)} ${"类型".padEnd(15)} ${"长度".padEnd(8)} ${"可空".padEnd(6)} 注释`,
|
|
194
|
+
"-".repeat(75),
|
|
195
|
+
];
|
|
196
|
+
for (const col of columns) {
|
|
197
|
+
const opts = col.nodeOptions ?? {};
|
|
198
|
+
lines.push(`${col.nodeName.padEnd(25)} ${(opts.dataType ?? "").padEnd(15)} ${(opts.dataLength ?? "").padEnd(8)} ${(opts.isNullable ? "是" : "否").padEnd(6)} ${opts.comments ?? ""}`);
|
|
199
|
+
}
|
|
200
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
201
|
+
});
|
|
202
|
+
// ── 工具四:执行 SQL ──────────────────────────────────────────────
|
|
203
|
+
server.tool("execute_sql", `在指定数据库上执行 SQL 语句,返回查询结果或执行状态。
|
|
204
|
+
|
|
205
|
+
用途:直接运行 SQL 查询数据,或执行 DML(INSERT/UPDATE/DELETE)操作。
|
|
206
|
+
参数说明:
|
|
207
|
+
- connection_id / connection_type:从 list_databases 获取
|
|
208
|
+
- database:数据库名称
|
|
209
|
+
- schema:SQL 执行的默认 Schema 上下文(即 search_path),默认 public
|
|
210
|
+
- sql:SQL 语句,支持 SELECT / INSERT / UPDATE / DELETE
|
|
211
|
+
返回:
|
|
212
|
+
- SELECT:列名表头 + 数据行(最多返回 500 行),以及总行数和执行耗时
|
|
213
|
+
- DML:影响行数和执行耗时
|
|
214
|
+
- 错误:具体错误信息
|
|
215
|
+
注意:一次只执行一条 SQL 语句。`, {
|
|
216
|
+
connection_id: z.number().describe("数据库连接ID,从 list_databases 获取"),
|
|
217
|
+
connection_type: z.string().default("PostgreSQL").describe("数据库类型,如 PostgreSQL、MySQL"),
|
|
218
|
+
database: z.string().describe("数据库名称"),
|
|
219
|
+
schema: z.string().default("public").describe("执行 SQL 的默认 Schema,默认 public"),
|
|
220
|
+
sql: z.string().describe("SQL 语句,一次执行一条"),
|
|
221
|
+
}, async ({ connection_id, connection_type, database, schema, sql }) => {
|
|
222
|
+
const tabKey = `mcp-${Date.now()}`;
|
|
223
|
+
const preview = sql.length > 60 ? sql.slice(0, 60) + "..." : sql;
|
|
224
|
+
log.info(`execute_sql: [${database}.${schema}] ${preview}`);
|
|
225
|
+
const data = await post("/dms/segment/statement/blocking/execute", {
|
|
226
|
+
connectionId: connection_id,
|
|
227
|
+
dataSourceType: connection_type,
|
|
228
|
+
databaseName: database,
|
|
229
|
+
operatingObject: schema,
|
|
230
|
+
statements: [sql],
|
|
231
|
+
offset: 0,
|
|
232
|
+
rowCount: 500,
|
|
233
|
+
tabKey,
|
|
234
|
+
plSql: false,
|
|
235
|
+
sortModels: null,
|
|
236
|
+
filterModel: null,
|
|
237
|
+
autoCommit: false,
|
|
238
|
+
actionType: null,
|
|
239
|
+
});
|
|
240
|
+
const infos = data?.executionInfos ?? [];
|
|
241
|
+
const lines = [];
|
|
242
|
+
for (const info of infos) {
|
|
243
|
+
const log_msg = info.executeLogInfo?.message;
|
|
244
|
+
const resp = info.response;
|
|
245
|
+
if (!resp?.success) {
|
|
246
|
+
const errMsg = resp?.executeError?.message ?? log_msg?.error ?? "执行失败";
|
|
247
|
+
log.error(`SQL 执行失败: ${errMsg}`);
|
|
248
|
+
lines.push(`错误: ${errMsg}`);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
// DML(无结果集)
|
|
252
|
+
if (!resp.resultData?.length) {
|
|
253
|
+
const affected = log_msg?.affectedRows ?? 0;
|
|
254
|
+
const ms = log_msg?.duration ?? 0;
|
|
255
|
+
log.info(` └─ 执行成功,影响 ${affected} 行,${ms}ms`);
|
|
256
|
+
lines.push(`执行成功\n影响行数: ${affected}\n耗时: ${ms}ms`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
// SELECT
|
|
260
|
+
const resultData = resp.resultData;
|
|
261
|
+
const colInfos = resp.columnInfos ?? [];
|
|
262
|
+
const colNames = colInfos.length
|
|
263
|
+
? colInfos.map((c) => c.columnName)
|
|
264
|
+
: Object.keys(resultData[0]);
|
|
265
|
+
const ms = log_msg?.duration ?? 0;
|
|
266
|
+
log.info(` └─ 返回 ${resultData.length} 行,${ms}ms`);
|
|
267
|
+
lines.push(colNames.join(" | "));
|
|
268
|
+
lines.push("-".repeat(Math.max(colNames.join(" | ").length, 20)));
|
|
269
|
+
for (const row of resultData) {
|
|
270
|
+
lines.push(colNames.map((col) => String(row[col]?.value ?? "")).join(" | "));
|
|
271
|
+
}
|
|
272
|
+
lines.push(`\n共 ${resultData.length} 行 耗时 ${ms}ms`);
|
|
273
|
+
}
|
|
274
|
+
return { content: [{ type: "text", text: lines.join("\n") || "无输出" }] };
|
|
275
|
+
});
|
|
276
|
+
// ── 启动 ──────────────────────────────────────────────────────────
|
|
277
|
+
log.info(`CloudQuery MCP Server 启动 BASE_URL=${BASE_URL} USER=${USERNAME}`);
|
|
278
|
+
const transport = new StdioServerTransport();
|
|
279
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cq-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP Server for CloudQuery platform — list databases, list tables, inspect columns, and execute SQL via MCP tools.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cq-mcp-server": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"cloudquery",
|
|
22
|
+
"database",
|
|
23
|
+
"sql",
|
|
24
|
+
"model-context-protocol"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
29
|
+
"node-forge": "^1.3.3",
|
|
30
|
+
"zod": "^4.3.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.3.2",
|
|
34
|
+
"@types/node-forge": "^1.3.14",
|
|
35
|
+
"tsx": "^4.0.0",
|
|
36
|
+
"typescript": "^5.0.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
}
|
|
41
|
+
}
|