cq-mcp-server 0.3.2 → 0.3.4
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 +2 -2
- package/dist/index.js +257 -136
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,9 +14,9 @@ npm install -g cq-mcp-server
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
# 直接运行,默认 stdio
|
|
17
|
-
CQ_BASE_URL=http://10.10.2.73 CQ_USERNAME=
|
|
17
|
+
CQ_BASE_URL=http://10.10.2.73 CQ_USERNAME=xxxxx CQ_PASSWORD=xxxx cq-mcp-server
|
|
18
18
|
```
|
|
19
|
-
|
|
19
|
+
s
|
|
20
20
|
### HTTP 模式(Dify 接入)
|
|
21
21
|
|
|
22
22
|
Dify 只支持 HTTP 传输,通过环境变量切换:
|
package/dist/index.js
CHANGED
|
@@ -122,11 +122,139 @@ function createSessionContext() {
|
|
|
122
122
|
metaNode,
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
|
+
async function implConnect(ctx, args) {
|
|
126
|
+
ctx.setConfig({ baseUrl: args.base_url.replace(/\/$/, ""), username: args.username, password: args.password });
|
|
127
|
+
log.info(`connect: 配置已更新 baseUrl=${args.base_url} user=${args.username}`);
|
|
128
|
+
return `连接配置已设置:${args.base_url},用户:${args.username}`;
|
|
129
|
+
}
|
|
130
|
+
async function implListDatabases(ctx) {
|
|
131
|
+
log.info("list_databases: 查询连接列表");
|
|
132
|
+
const connections = await ctx.metaNode({ nodeType: "root", nodePath: "/root" });
|
|
133
|
+
if (!connections.length)
|
|
134
|
+
return "未找到任何数据库连接";
|
|
135
|
+
const lines = [];
|
|
136
|
+
for (const conn of connections) {
|
|
137
|
+
const { connectionId, connectionType, nodeName } = conn;
|
|
138
|
+
lines.push(`## 连接: ${nodeName} (connection_id=${connectionId}, connection_type=${connectionType})`);
|
|
139
|
+
const databases = await ctx.metaNode({
|
|
140
|
+
nodeType: "connection",
|
|
141
|
+
nodePath: `/root/${connectionId}`,
|
|
142
|
+
nodePathWithType: `/CONNECTION:${connectionId}`,
|
|
143
|
+
connectionId,
|
|
144
|
+
connectionType,
|
|
145
|
+
});
|
|
146
|
+
log.info(` └─ ${nodeName}: ${databases.length} 个数据库`);
|
|
147
|
+
if (databases.length) {
|
|
148
|
+
for (const db of databases)
|
|
149
|
+
lines.push(` - ${db.nodeName}`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
lines.push(" (无数据库)");
|
|
153
|
+
}
|
|
154
|
+
lines.push("");
|
|
155
|
+
}
|
|
156
|
+
return lines.join("\n");
|
|
157
|
+
}
|
|
158
|
+
async function implListTables(ctx, args) {
|
|
159
|
+
const schema = args.schema ?? "public";
|
|
160
|
+
log.info(`list_tables: ${args.database}.${schema} (conn=${args.connection_id})`);
|
|
161
|
+
const tables = await ctx.metaNode({
|
|
162
|
+
nodeType: "tableGroup",
|
|
163
|
+
nodePath: `/root/${args.connection_id}/${args.database}/${schema}/tables`,
|
|
164
|
+
nodePathWithType: `/CONNECTION:${args.connection_id}/DATABASE:${args.database}/SCHEMA:${schema}`,
|
|
165
|
+
connectionId: args.connection_id,
|
|
166
|
+
connectionType: args.connection_type,
|
|
167
|
+
});
|
|
168
|
+
if (!tables.length)
|
|
169
|
+
return `${args.database}.${schema} 下未找到任何表`;
|
|
170
|
+
log.info(` └─ 共 ${tables.length} 张表`);
|
|
171
|
+
const lines = [`${args.database}.${schema} 共 ${tables.length} 张表:`, ""];
|
|
172
|
+
for (const t of tables)
|
|
173
|
+
lines.push(`- ${t.nodeName}`);
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
}
|
|
176
|
+
async function implGetTableColumns(ctx, args) {
|
|
177
|
+
log.info(`get_table_columns: ${args.database}.${args.schema}.${args.table} (conn=${args.connection_id})`);
|
|
178
|
+
const columns = await ctx.metaNode({
|
|
179
|
+
nodeType: "columnGroup",
|
|
180
|
+
nodePath: `/root/${args.connection_id}/${args.database}/${args.schema}/tables/${args.table}/columns`,
|
|
181
|
+
nodePathWithType: `/CONNECTION:${args.connection_id}/DATABASE:${args.database}/SCHEMA:${args.schema}/TABLE:${args.table}`,
|
|
182
|
+
connectionId: args.connection_id,
|
|
183
|
+
connectionType: args.connection_type,
|
|
184
|
+
});
|
|
185
|
+
if (!columns.length)
|
|
186
|
+
return `表 ${args.database}.${args.schema}.${args.table} 未找到字段信息`;
|
|
187
|
+
log.info(` └─ ${columns.length} 个字段`);
|
|
188
|
+
const lines = [
|
|
189
|
+
`表 ${args.database}.${args.schema}.${args.table} 共 ${columns.length} 个字段:`,
|
|
190
|
+
"",
|
|
191
|
+
`${"字段名".padEnd(25)} ${"类型".padEnd(15)} ${"长度".padEnd(8)} ${"可空".padEnd(6)} 注释`,
|
|
192
|
+
"-".repeat(75),
|
|
193
|
+
];
|
|
194
|
+
for (const col of columns) {
|
|
195
|
+
const opts = col.nodeOptions ?? {};
|
|
196
|
+
lines.push(`${col.nodeName.padEnd(25)} ${(opts.dataType ?? "").padEnd(15)} ${(opts.dataLength ?? "").padEnd(8)} ${(opts.isNullable ? "是" : "否").padEnd(6)} ${opts.comments ?? ""}`);
|
|
197
|
+
}
|
|
198
|
+
return lines.join("\n");
|
|
199
|
+
}
|
|
200
|
+
async function implExecuteSql(ctx, args) {
|
|
201
|
+
const connection_type = args.connection_type ?? "PostgreSQL";
|
|
202
|
+
const schema = args.schema ?? "public";
|
|
203
|
+
const tabKey = `mcp-${Date.now()}`;
|
|
204
|
+
const preview = args.sql.length > 60 ? args.sql.slice(0, 60) + "..." : args.sql;
|
|
205
|
+
log.info(`execute_sql: [${args.database}.${schema}] ${preview}`);
|
|
206
|
+
const data = await ctx.post("/dms/segment/statement/blocking/execute", {
|
|
207
|
+
connectionId: args.connection_id,
|
|
208
|
+
dataSourceType: connection_type,
|
|
209
|
+
databaseName: args.database,
|
|
210
|
+
operatingObject: schema,
|
|
211
|
+
statements: [args.sql],
|
|
212
|
+
offset: 0,
|
|
213
|
+
rowCount: 500,
|
|
214
|
+
tabKey,
|
|
215
|
+
plSql: false,
|
|
216
|
+
sortModels: null,
|
|
217
|
+
filterModel: null,
|
|
218
|
+
autoCommit: false,
|
|
219
|
+
actionType: null,
|
|
220
|
+
});
|
|
221
|
+
const infos = data?.executionInfos ?? [];
|
|
222
|
+
const lines = [];
|
|
223
|
+
for (const info of infos) {
|
|
224
|
+
const log_msg = info.executeLogInfo?.message;
|
|
225
|
+
const resp = info.response;
|
|
226
|
+
if (!resp?.success || log_msg?.success === false) {
|
|
227
|
+
const errMsg = log_msg?.error ?? resp?.executeError?.message ?? "执行失败";
|
|
228
|
+
log.error(`SQL 执行失败: ${errMsg}`);
|
|
229
|
+
lines.push(`错误: ${errMsg}`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (!resp.resultData?.length) {
|
|
233
|
+
const affected = log_msg?.affectedRows ?? 0;
|
|
234
|
+
const ms = log_msg?.duration ?? 0;
|
|
235
|
+
log.info(` └─ 执行成功,影响 ${affected} 行,${ms}ms`);
|
|
236
|
+
lines.push(`执行成功\n影响行数: ${affected}\n耗时: ${ms}ms`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const resultData = resp.resultData;
|
|
240
|
+
const colInfos = resp.columnInfos ?? [];
|
|
241
|
+
const colNames = colInfos.length ? colInfos.map((c) => c.columnName) : Object.keys(resultData[0]);
|
|
242
|
+
const ms = log_msg?.duration ?? 0;
|
|
243
|
+
log.info(` └─ 返回 ${resultData.length} 行,${ms}ms`);
|
|
244
|
+
const rows = resultData.map((row) => {
|
|
245
|
+
const obj = {};
|
|
246
|
+
for (const col of colNames)
|
|
247
|
+
obj[col] = row[col]?.value ?? null;
|
|
248
|
+
return obj;
|
|
249
|
+
});
|
|
250
|
+
lines.push(JSON.stringify({ columns: colNames, rows, total: resultData.length, elapsed_ms: ms }));
|
|
251
|
+
}
|
|
252
|
+
return lines.join("\n") || "无输出";
|
|
253
|
+
}
|
|
125
254
|
// ── 工具注册工厂(每个 session 独立) ─────────────────────────────
|
|
126
255
|
function createMcpServer() {
|
|
127
256
|
const server = new McpServer({ name: "cq-mcp-server", version: "0.2.0" });
|
|
128
257
|
const ctx = createSessionContext();
|
|
129
|
-
// ── 工具零:连接配置 ────────────────────────────────────────────
|
|
130
258
|
server.tool("connect", `配置 CloudQuery 平台的连接信息(地址、账号、密码)。
|
|
131
259
|
|
|
132
260
|
用途:在调用其他工具前,先通过此工具设置连接参数。适用于未配置环境变量的场景(如 Dify、动态多租户等)。
|
|
@@ -139,47 +267,12 @@ function createMcpServer() {
|
|
|
139
267
|
base_url: z.string().describe("CloudQuery 平台地址,如 http://10.10.2.73"),
|
|
140
268
|
username: z.string().describe("登录账号"),
|
|
141
269
|
password: z.string().describe("登录密码"),
|
|
142
|
-
}, async ({
|
|
143
|
-
ctx.setConfig({ baseUrl: base_url.replace(/\/$/, ""), username, password });
|
|
144
|
-
log.info(`connect: 配置已更新 baseUrl=${base_url} user=${username}`);
|
|
145
|
-
return {
|
|
146
|
-
content: [{ type: "text", text: `连接配置已设置:${base_url},用户:${username}` }],
|
|
147
|
-
};
|
|
148
|
-
});
|
|
149
|
-
// ── 工具一:查询数据库清单 ───────────────────────────────────────
|
|
270
|
+
}, async (args) => ({ content: [{ type: "text", text: await implConnect(ctx, args) }] }));
|
|
150
271
|
server.tool("list_databases", `查询所有数据库连接及其下的数据库实例列表。
|
|
151
272
|
|
|
152
273
|
用途:了解当前 CloudQuery 平台上有哪些数据库连接,以及每个连接下有哪些数据库。
|
|
153
274
|
返回:连接名称、连接ID、数据库类型,以及每个连接下的数据库列表。
|
|
154
|
-
后续:拿到 connection_id 和 connection_type 后,可调用 list_tables 查看具体库的表。`, {}, async () => {
|
|
155
|
-
log.info("list_databases: 查询连接列表");
|
|
156
|
-
const connections = await ctx.metaNode({ nodeType: "root", nodePath: "/root" });
|
|
157
|
-
if (!connections.length)
|
|
158
|
-
return { content: [{ type: "text", text: "未找到任何数据库连接" }] };
|
|
159
|
-
const lines = [];
|
|
160
|
-
for (const conn of connections) {
|
|
161
|
-
const { connectionId, connectionType, nodeName } = conn;
|
|
162
|
-
lines.push(`## 连接: ${nodeName} (connection_id=${connectionId}, connection_type=${connectionType})`);
|
|
163
|
-
const databases = await ctx.metaNode({
|
|
164
|
-
nodeType: "connection",
|
|
165
|
-
nodePath: `/root/${connectionId}`,
|
|
166
|
-
nodePathWithType: `/CONNECTION:${connectionId}`,
|
|
167
|
-
connectionId,
|
|
168
|
-
connectionType,
|
|
169
|
-
});
|
|
170
|
-
log.info(` └─ ${nodeName}: ${databases.length} 个数据库`);
|
|
171
|
-
if (databases.length) {
|
|
172
|
-
for (const db of databases)
|
|
173
|
-
lines.push(` - ${db.nodeName}`);
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
lines.push(" (无数据库)");
|
|
177
|
-
}
|
|
178
|
-
lines.push("");
|
|
179
|
-
}
|
|
180
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
181
|
-
});
|
|
182
|
-
// ── 工具二:查询表清单 ──────────────────────────────────────────
|
|
275
|
+
后续:拿到 connection_id 和 connection_type 后,可调用 list_tables 查看具体库的表。`, {}, async () => ({ content: [{ type: "text", text: await implListDatabases(ctx) }] }));
|
|
183
276
|
server.tool("list_tables", `查询指定数据库 Schema 下的所有普通表。
|
|
184
277
|
|
|
185
278
|
用途:了解某个数据库 Schema 下有哪些表,为后续字段查询或 SQL 编写做准备。
|
|
@@ -193,25 +286,7 @@ function createMcpServer() {
|
|
|
193
286
|
connection_type: z.string().describe("数据库类型,如 PostgreSQL、MySQL,从 list_databases 获取"),
|
|
194
287
|
database: z.string().describe("数据库名称,如 pam、postgres"),
|
|
195
288
|
schema: z.string().default("public").describe("Schema 名称,PostgreSQL 默认 public"),
|
|
196
|
-
}, async ({
|
|
197
|
-
log.info(`list_tables: ${database}.${schema} (conn=${connection_id})`);
|
|
198
|
-
const tables = await ctx.metaNode({
|
|
199
|
-
nodeType: "tableGroup",
|
|
200
|
-
nodePath: `/root/${connection_id}/${database}/${schema}/tables`,
|
|
201
|
-
nodePathWithType: `/CONNECTION:${connection_id}/DATABASE:${database}/SCHEMA:${schema}`,
|
|
202
|
-
connectionId: connection_id,
|
|
203
|
-
connectionType: connection_type,
|
|
204
|
-
});
|
|
205
|
-
if (!tables.length) {
|
|
206
|
-
return { content: [{ type: "text", text: `${database}.${schema} 下未找到任何表` }] };
|
|
207
|
-
}
|
|
208
|
-
log.info(` └─ 共 ${tables.length} 张表`);
|
|
209
|
-
const lines = [`${database}.${schema} 共 ${tables.length} 张表:`, ""];
|
|
210
|
-
for (const t of tables)
|
|
211
|
-
lines.push(`- ${t.nodeName}`);
|
|
212
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
213
|
-
});
|
|
214
|
-
// ── 工具三:查询表字段 ──────────────────────────────────────────
|
|
289
|
+
}, async (args) => ({ content: [{ type: "text", text: await implListTables(ctx, args) }] }));
|
|
215
290
|
server.tool("get_table_columns", `查询指定表的完整字段定义,包括字段名、数据类型、长度、是否可空、业务注释。
|
|
216
291
|
|
|
217
292
|
用途:了解表结构,辅助编写精确的 SQL 查询语句。
|
|
@@ -225,32 +300,7 @@ function createMcpServer() {
|
|
|
225
300
|
database: z.string().describe("数据库名称"),
|
|
226
301
|
schema: z.string().describe("Schema 名称"),
|
|
227
302
|
table: z.string().describe("表名,从 list_tables 获取"),
|
|
228
|
-
}, async ({
|
|
229
|
-
log.info(`get_table_columns: ${database}.${schema}.${table} (conn=${connection_id})`);
|
|
230
|
-
const columns = await ctx.metaNode({
|
|
231
|
-
nodeType: "columnGroup",
|
|
232
|
-
nodePath: `/root/${connection_id}/${database}/${schema}/tables/${table}/columns`,
|
|
233
|
-
nodePathWithType: `/CONNECTION:${connection_id}/DATABASE:${database}/SCHEMA:${schema}/TABLE:${table}`,
|
|
234
|
-
connectionId: connection_id,
|
|
235
|
-
connectionType: connection_type,
|
|
236
|
-
});
|
|
237
|
-
if (!columns.length) {
|
|
238
|
-
return { content: [{ type: "text", text: `表 ${database}.${schema}.${table} 未找到字段信息` }] };
|
|
239
|
-
}
|
|
240
|
-
log.info(` └─ ${columns.length} 个字段`);
|
|
241
|
-
const lines = [
|
|
242
|
-
`表 ${database}.${schema}.${table} 共 ${columns.length} 个字段:`,
|
|
243
|
-
"",
|
|
244
|
-
`${"字段名".padEnd(25)} ${"类型".padEnd(15)} ${"长度".padEnd(8)} ${"可空".padEnd(6)} 注释`,
|
|
245
|
-
"-".repeat(75),
|
|
246
|
-
];
|
|
247
|
-
for (const col of columns) {
|
|
248
|
-
const opts = col.nodeOptions ?? {};
|
|
249
|
-
lines.push(`${col.nodeName.padEnd(25)} ${(opts.dataType ?? "").padEnd(15)} ${(opts.dataLength ?? "").padEnd(8)} ${(opts.isNullable ? "是" : "否").padEnd(6)} ${opts.comments ?? ""}`);
|
|
250
|
-
}
|
|
251
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
252
|
-
});
|
|
253
|
-
// ── 工具四:执行 SQL ────────────────────────────────────────────
|
|
303
|
+
}, async (args) => ({ content: [{ type: "text", text: await implGetTableColumns(ctx, args) }] }));
|
|
254
304
|
server.tool("execute_sql", `在指定数据库上执行 SQL 语句,返回查询结果或执行状态。
|
|
255
305
|
|
|
256
306
|
用途:直接运行 SQL 查询数据,或执行 DML(INSERT/UPDATE/DELETE)操作。
|
|
@@ -269,59 +319,7 @@ function createMcpServer() {
|
|
|
269
319
|
database: z.string().describe("数据库名称"),
|
|
270
320
|
schema: z.string().default("public").describe("执行 SQL 的默认 Schema,默认 public"),
|
|
271
321
|
sql: z.string().describe("SQL 语句,一次执行一条"),
|
|
272
|
-
}, async ({
|
|
273
|
-
const tabKey = `mcp-${Date.now()}`;
|
|
274
|
-
const preview = sql.length > 60 ? sql.slice(0, 60) + "..." : sql;
|
|
275
|
-
log.info(`execute_sql: [${database}.${schema}] ${preview}`);
|
|
276
|
-
const data = await ctx.post("/dms/segment/statement/blocking/execute", {
|
|
277
|
-
connectionId: connection_id,
|
|
278
|
-
dataSourceType: connection_type,
|
|
279
|
-
databaseName: database,
|
|
280
|
-
operatingObject: schema,
|
|
281
|
-
statements: [sql],
|
|
282
|
-
offset: 0,
|
|
283
|
-
rowCount: 500,
|
|
284
|
-
tabKey,
|
|
285
|
-
plSql: false,
|
|
286
|
-
sortModels: null,
|
|
287
|
-
filterModel: null,
|
|
288
|
-
autoCommit: false,
|
|
289
|
-
actionType: null,
|
|
290
|
-
});
|
|
291
|
-
const infos = data?.executionInfos ?? [];
|
|
292
|
-
const lines = [];
|
|
293
|
-
for (const info of infos) {
|
|
294
|
-
const log_msg = info.executeLogInfo?.message;
|
|
295
|
-
const resp = info.response;
|
|
296
|
-
if (!resp?.success) {
|
|
297
|
-
const errMsg = resp?.executeError?.message ?? log_msg?.error ?? "执行失败";
|
|
298
|
-
log.error(`SQL 执行失败: ${errMsg}`);
|
|
299
|
-
lines.push(`错误: ${errMsg}`);
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (!resp.resultData?.length) {
|
|
303
|
-
const affected = log_msg?.affectedRows ?? 0;
|
|
304
|
-
const ms = log_msg?.duration ?? 0;
|
|
305
|
-
log.info(` └─ 执行成功,影响 ${affected} 行,${ms}ms`);
|
|
306
|
-
lines.push(`执行成功\n影响行数: ${affected}\n耗时: ${ms}ms`);
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
const resultData = resp.resultData;
|
|
310
|
-
const colInfos = resp.columnInfos ?? [];
|
|
311
|
-
const colNames = colInfos.length
|
|
312
|
-
? colInfos.map((c) => c.columnName)
|
|
313
|
-
: Object.keys(resultData[0]);
|
|
314
|
-
const ms = log_msg?.duration ?? 0;
|
|
315
|
-
log.info(` └─ 返回 ${resultData.length} 行,${ms}ms`);
|
|
316
|
-
lines.push(colNames.join(" | "));
|
|
317
|
-
lines.push("-".repeat(Math.max(colNames.join(" | ").length, 20)));
|
|
318
|
-
for (const row of resultData) {
|
|
319
|
-
lines.push(colNames.map((col) => String(row[col]?.value ?? "")).join(" | "));
|
|
320
|
-
}
|
|
321
|
-
lines.push(`\n共 ${resultData.length} 行 耗时 ${ms}ms`);
|
|
322
|
-
}
|
|
323
|
-
return { content: [{ type: "text", text: lines.join("\n") || "无输出" }] };
|
|
324
|
-
});
|
|
322
|
+
}, async (args) => ({ content: [{ type: "text", text: await implExecuteSql(ctx, args) }] }));
|
|
325
323
|
return server;
|
|
326
324
|
}
|
|
327
325
|
// ── 启动 ──────────────────────────────────────────────────────────
|
|
@@ -333,6 +331,20 @@ if (MCP_TRANSPORT === "http") {
|
|
|
333
331
|
const sessions = new Map();
|
|
334
332
|
// SSE transport 映射(sessionId → transport),用于兼容 Dify 等旧版客户端
|
|
335
333
|
const sseSessions = new Map();
|
|
334
|
+
// /invoke 端点使用的全局 session context(持久化凭证,供 Dify 调用)
|
|
335
|
+
const invokeCtx = createSessionContext();
|
|
336
|
+
/** 读取请求体 */
|
|
337
|
+
function readBody(req) {
|
|
338
|
+
return new Promise((resolve, reject) => {
|
|
339
|
+
const chunks = [];
|
|
340
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
341
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
342
|
+
req.on("error", reject);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
// Schema 缓存(10 分钟 TTL,避免每次请求都重新查询所有表)
|
|
346
|
+
let schemaCache = null;
|
|
347
|
+
let schemaCacheTime = 0;
|
|
336
348
|
const httpServer = createServer(async (req, res) => {
|
|
337
349
|
// 允许跨域(Dify 可能从不同域访问)
|
|
338
350
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -343,6 +355,115 @@ if (MCP_TRANSPORT === "http") {
|
|
|
343
355
|
res.writeHead(204).end();
|
|
344
356
|
return;
|
|
345
357
|
}
|
|
358
|
+
// ── /schema 端点:一次性返回所有数据库+表结构,供 LLM 参考生成 SQL ──
|
|
359
|
+
if (req.url === "/schema" && req.method === "GET") {
|
|
360
|
+
// 返回缓存(10分钟有效),避免每次重新查询所有表
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
if (schemaCache && now - schemaCacheTime < 10 * 60 * 1000) {
|
|
363
|
+
log.info(`/schema: 命中缓存 (${Math.round((now - schemaCacheTime) / 1000)}s前)`);
|
|
364
|
+
res.writeHead(200, { "Content-Type": "application/json" }).end(schemaCache);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
// Parse connection info from list_databases text output
|
|
369
|
+
const dbText = await implListDatabases(invokeCtx);
|
|
370
|
+
// Extract connection blocks: "## 连接: name (connection_id=N, connection_type=T)\n - db1\n - db2"
|
|
371
|
+
const result = [];
|
|
372
|
+
const connBlocks = dbText.split(/^## /m).filter(Boolean);
|
|
373
|
+
for (const block of connBlocks) {
|
|
374
|
+
const headerMatch = block.match(/^连接: (\S+)\s+\(connection_id=(\d+), connection_type=(\w+)\)/);
|
|
375
|
+
if (!headerMatch)
|
|
376
|
+
continue;
|
|
377
|
+
const [, connName, connIdStr, connType] = headerMatch;
|
|
378
|
+
const connectionId = parseInt(connIdStr, 10);
|
|
379
|
+
const dbNames = [...block.matchAll(/^ - (.+)$/mg)].map(m => m[1].trim());
|
|
380
|
+
const databases = [];
|
|
381
|
+
for (const dbName of dbNames) {
|
|
382
|
+
let found = false;
|
|
383
|
+
for (const schema of [dbName, "public"]) {
|
|
384
|
+
try {
|
|
385
|
+
const tablesText = await implListTables(invokeCtx, {
|
|
386
|
+
connection_id: connectionId,
|
|
387
|
+
connection_type: connType,
|
|
388
|
+
database: dbName,
|
|
389
|
+
schema,
|
|
390
|
+
});
|
|
391
|
+
if (!tablesText.includes("未找到任何表")) {
|
|
392
|
+
// Parse table names from the result text
|
|
393
|
+
const tableNames = [...tablesText.matchAll(/^- (.+)$/mg)].map(m => m[1].trim());
|
|
394
|
+
databases.push({ name: dbName, schema, tables: tableNames });
|
|
395
|
+
found = true;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Skip schemas that error (e.g., not enabled)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!found)
|
|
404
|
+
databases.push({ name: dbName, schema: dbName, tables: [] });
|
|
405
|
+
}
|
|
406
|
+
result.push({ connection_id: connectionId, connection_type: connType, name: connName, databases });
|
|
407
|
+
}
|
|
408
|
+
const body = JSON.stringify({ schema: result });
|
|
409
|
+
schemaCache = body;
|
|
410
|
+
schemaCacheTime = Date.now();
|
|
411
|
+
log.info(`/schema: 已缓存 (${result.length} 个连接)`);
|
|
412
|
+
res.writeHead(200, { "Content-Type": "application/json" }).end(body);
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ error: String(err) }));
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// ── /invoke 端点:简单 REST API,返回纯 JSON,供 Dify 等直接调用 ──
|
|
420
|
+
if (req.url === "/invoke" && req.method === "POST") {
|
|
421
|
+
const body = await readBody(req);
|
|
422
|
+
let parsed;
|
|
423
|
+
try {
|
|
424
|
+
parsed = JSON.parse(body);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "Invalid JSON" }));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const { tool, args = {} } = parsed;
|
|
431
|
+
log.info(`/invoke: tool=${tool} args=${JSON.stringify(args).slice(0, 200)}`);
|
|
432
|
+
try {
|
|
433
|
+
let result;
|
|
434
|
+
switch (tool) {
|
|
435
|
+
case "connect":
|
|
436
|
+
result = await implConnect(invokeCtx, args);
|
|
437
|
+
break;
|
|
438
|
+
case "list_databases":
|
|
439
|
+
result = await implListDatabases(invokeCtx);
|
|
440
|
+
break;
|
|
441
|
+
case "list_tables":
|
|
442
|
+
result = await implListTables(invokeCtx, args);
|
|
443
|
+
break;
|
|
444
|
+
case "get_table_columns":
|
|
445
|
+
result = await implGetTableColumns(invokeCtx, args);
|
|
446
|
+
break;
|
|
447
|
+
case "execute_sql": {
|
|
448
|
+
// 只执行第一条 SQL 语句(LLM 有时会输出多条)
|
|
449
|
+
const sqlArgs = args;
|
|
450
|
+
if (typeof sqlArgs.sql === "string") {
|
|
451
|
+
sqlArgs.sql = sqlArgs.sql.split(/;[\s\n]+(?=\S)/)[0].replace(/;[\s]*$/, "").trim();
|
|
452
|
+
}
|
|
453
|
+
result = await implExecuteSql(invokeCtx, sqlArgs);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
default:
|
|
457
|
+
result = `未知工具: ${tool}。可用工具: connect, list_databases, list_tables, get_table_columns, execute_sql`;
|
|
458
|
+
}
|
|
459
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }).end(result);
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
log.error(`/invoke error: ${err}`);
|
|
463
|
+
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ error: String(err) }));
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
346
467
|
if (!req.url?.startsWith("/mcp")) {
|
|
347
468
|
res.writeHead(404).end("Not Found");
|
|
348
469
|
return;
|
package/package.json
CHANGED