bfi-cli 0.1.69 → 0.1.72

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.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ import module from 'node:module';
4
+
5
+ if (module.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) {
6
+ try {
7
+ module.enableCompileCache();
8
+ } catch {
9
+ // Ignore errors
10
+ }
11
+ }
12
+
13
+ await import('../dist/mcp-db/main.mjs');
package/dist/cli.mjs CHANGED
@@ -7996,15 +7996,23 @@ const YMC_DEFAULT_BASE = process.env.BFI_YMC_BASE || "http://172.20.35.178:8901/
7996
7996
  var YmcApi = class {
7997
7997
  baseUrl;
7998
7998
  token;
7999
- constructor(token = null, baseUrl) {
7999
+ dryRun;
8000
+ constructor(token = null, baseUrl, dryRun = false) {
8000
8001
  this.baseUrl = (baseUrl || YMC_DEFAULT_BASE).replace(/\/$/, "");
8002
+ this.dryRun = dryRun;
8001
8003
  if (token) {
8002
8004
  this.token = token;
8003
8005
  return;
8004
8006
  }
8005
8007
  this.token = loadGlobalConfig()?.token ?? null;
8006
8008
  }
8009
+ _dryRunExit(method, path, body) {
8010
+ console.log(`[dry-run] ${method} ${this.baseUrl}/${path.replace(/^\//, "")}`);
8011
+ if (body !== void 0 && Object.keys(body).length > 0) console.log(JSON.stringify(body, null, 2));
8012
+ process.exit(0);
8013
+ }
8007
8014
  async call(path, body = {}, timeoutMs = 9e4) {
8015
+ if (this.dryRun) this._dryRunExit("POST", path, body);
8008
8016
  const url = `${this.baseUrl}/${path.replace(/^\//, "")}`;
8009
8017
  const payload = JSON.stringify(body);
8010
8018
  const mod = url.startsWith("https://") ? https : http;
@@ -8045,6 +8053,11 @@ var YmcApi = class {
8045
8053
  };
8046
8054
  }
8047
8055
  async download(path, body, outFile, timeoutMs = 18e4) {
8056
+ if (this.dryRun) {
8057
+ console.log(`[dry-run] POST ${this.baseUrl}/${path.replace(/^\//, "")} (响应写盘 → ${outFile})`);
8058
+ if (Object.keys(body).length > 0) console.log(JSON.stringify(body, null, 2));
8059
+ process.exit(0);
8060
+ }
8048
8061
  const url = `${this.baseUrl}/${path.replace(/^\//, "")}`;
8049
8062
  const payload = JSON.stringify(body);
8050
8063
  const mod = url.startsWith("https://") ? https : http;
@@ -8078,6 +8091,7 @@ var YmcApi = class {
8078
8091
  return raw.length;
8079
8092
  }
8080
8093
  async get(path, timeoutMs = 3e4) {
8094
+ if (this.dryRun) this._dryRunExit("GET", path);
8081
8095
  const url = `${this.baseUrl}/${path.replace(/^\//, "")}`;
8082
8096
  const mod = url.startsWith("https://") ? https : http;
8083
8097
  const raw = await new Promise((resolve, reject) => {
@@ -8189,13 +8203,22 @@ async function runYmc(args, globalOpts) {
8189
8203
  const sub = args[0];
8190
8204
  const rest = args.slice(1);
8191
8205
  const jsonMode = flag(rest, "--json");
8206
+ const dryRun = flag(rest, "--dry-run");
8192
8207
  const token = globalOpts.token;
8193
- const api = new YmcApi(token ?? null);
8194
- requireToken(api);
8208
+ const api = new YmcApi(token ?? null, void 0, dryRun);
8195
8209
  if (!sub || sub === "--help" || sub === "-h") {
8196
8210
  printHelp();
8197
8211
  return;
8198
8212
  }
8213
+ if (sub === "help") {
8214
+ printCommandHelp(rest[0] || "");
8215
+ return;
8216
+ }
8217
+ if (flag(rest, "--help") || flag(rest, "-h")) {
8218
+ printCommandHelp(sub);
8219
+ return;
8220
+ }
8221
+ if (!dryRun) requireToken(api);
8199
8222
  switch (sub) {
8200
8223
  case "status":
8201
8224
  emit(await api.get("ymc/auth/status"), jsonMode, (data) => {
@@ -8279,6 +8302,7 @@ async function runYmc(args, globalOpts) {
8279
8302
  ...opt(rest, "--days") ? { days: Number(opt(rest, "--days")) } : {},
8280
8303
  ...opt(rest, "--line") ? { line: Number(opt(rest, "--line")) } : {}
8281
8304
  };
8305
+ if (!opt(rest, "-t", "--time") && !opt(rest, "--start-time") && !opt(rest, "--days")) console.error("⚠️ 未指定时间窗:服务端按超大窗口兜底,命中可能含数天前记录——取证请显式 -t/--start-time");
8282
8306
  emit(await api.call("sls/raw", body), jsonMode, (data) => {
8283
8307
  const d = data;
8284
8308
  const rows = Array.isArray(d) ? d : d.rows ?? d.data ?? [];
@@ -8732,6 +8756,133 @@ async function runYmc(args, globalOpts) {
8732
8756
  process.exit(1);
8733
8757
  }
8734
8758
  }
8759
+ const HELP = {
8760
+ status: `bfi ymc status — 查询 YMC 登录会话状态
8761
+ 说明:显示 dev/prod 两个 host 组的登录态与过期时间。
8762
+ 注意:显示"未登录"不影响取数——首次取数会自动登录(多等几秒),不要停下。`,
8763
+ trace: `bfi ymc trace <trace_id> -e <env> [--ts <毫秒>] [--json]
8764
+ 三源合一查询:SLS 业务日志 + warningChain 链路 + ingress 访问日志(诊断首选入口)。
8765
+ 参数:
8766
+ <trace_id> 追踪 ID(全局唯一,跨服务,无需 appCode)
8767
+ -e <env> 环境:test/daily/pre/core1-4/commldev
8768
+ --ts <毫秒> Epoch 毫秒时间锚点;默认自动推导(SLS 首条日志时间),
8769
+ SLS 无日志时须显式提供
8770
+ --json 输出完整信封 {code,msg,data}
8771
+ 摘要含:span 数/异常数/最大耗时/慢调用与慢SQL告警数/Top慢span/ingress 入口。
8772
+ 注意:trace 摘要的入口 URL 可能混入同链路其他入口记录——与目标不符时核对 ingress 明细。`,
8773
+ chain: `bfi ymc chain <trace_id> -e <env> [--ts <毫秒>] [--json]
8774
+ 原子源:warningChain 全量 Span 链路(耗时分解/调用树)。trace 三源合一的链路通道单独调用。`,
8775
+ "sls-logs": `bfi ymc sls-logs <trace_id> -e <env> [--ts <毫秒>] [--json]
8776
+ 原子源:SLS 业务日志(异常全文/堆栈)。按 traceid 索引检索。`,
8777
+ "sls-raw": `bfi ymc sls-raw "<查询>" -e <env> [-t <time> | --start-time <s> --end-time <e> | --days N] [--line N] [--json]
8778
+ SLS 原始查询(阿里云 SLS 完整语法透传:索引检索 + | SELECT 分析语句)。
8779
+ 参数:
8780
+ "<查询>" 查询语句。核心规则:索引检索前置(traceid: xx / "关键词" AND applicationname: xx),
8781
+ 禁止 * | SELECT * WHERE 字段=... (WHERE 全扫描必超时)
8782
+ -t <time> 时间简写 10m/2h/3d/4w(默认无时间=服务端超大窗兜底,会命中数天前记录,务必显式指定)
8783
+ --start-time/--end-time 精确窗口 yyyy-MM-dd HH:mm:ss 北京时间(优先于 -t)
8784
+ --days N 按天数回溯(旧参数,等价 -t Nd)
8785
+ --line N 返回行数上限
8786
+ 示例:
8787
+ bfi ymc sls-raw 'traceid: abc | SELECT * LIMIT 200' -e daily -t 1d
8788
+ bfi ymc sls-raw '"Exception" AND applicationname: yonbip-scm-pu | SELECT count(*) AS cnt' -e pre -t 3d
8789
+ bfi ymc sls-raw 'applicationname: yonbip-scm-scmmp AND "unaudit" | SELECT traceid, time LIMIT 10' -e core3 -t 1d
8790
+ 注意:业务关键词检索必须 applicationname: 限定目标服务(共享路径 URL 全平台都有);
8791
+ traceid/spanid 检索例外(全局唯一)。非索引字段(logger/requesturl)不能进检索语法,只能在 SELECT 里引用。`,
8792
+ "sls-grep": `bfi ymc sls-grep "<关键词>" -e <env> -t <time> [--json]
8793
+ SLS 全文检索聚合(每组计数)。注意:计数上限 100 可能封顶,影响面/精确计数用 sls-raw + GROUP BY。`,
8794
+ slowsql: `bfi ymc slowsql -t <time> -e <env> [--sql-str <表名或片段>] [--md5 <指纹>] [--trace-id <id>] [--app <appCode>] [--no-profiler] [--page N] [--page-size N] [--json]
8795
+ 慢 SQL 告警明细(耗时降序;摘要含 服务/耗时/行数/SQL 前段/指纹/调用栈首帧)。
8796
+ 参数:
8797
+ -t <time> 时间窗(必填语义,服务端强校验)
8798
+ --sql-str 按 SQL 片段过滤(表名/语句片段)
8799
+ --md5 按 SQL MD5 指纹精确过滤(同类 SQL 归类)
8800
+ --trace-id 按链路过滤。⚠️ 按 trace 查询时不带 --app(traceId 已唯一限定,
8801
+ --app 会截断其他服务的告警行——慢 SQL 归属哪个服务事先未知)
8802
+ --app 服务维度过滤(无 traceId 时必带,裸 appCode)
8803
+ --no-profiler 显式关闭 enableProfiler(默认自动带 'true'——不带该字段上游 500,
8804
+ 带了才返回 sqlStack 调用栈)
8805
+ 示例:
8806
+ bfi ymc slowsql -t 1d --app yonbip-scm-scmmp -e core3 # 服务维度(SQL 画像)
8807
+ bfi ymc slowsql -t 1d --trace-id abc -e core3 # 链路维度(勿带 --app)`,
8808
+ slowcall: `bfi ymc slowcall -t <time> -e <env> [--trace-id <id>] [--app <appCode>] [--method <方法>] [--min-cost <ms>] [--page N] [--page-size N] [--json]
8809
+ 慢调用告警明细(耗时降序;摘要含 服务/耗时/sql/rpc/redis 分解/方法/pod)。
8810
+ 参数:
8811
+ --trace-id 按链路过滤。⚠️ 不带 --app(同 slowsql,--app 会截断下游服务告警行)
8812
+ --app 服务维度过滤(裸 appCode)
8813
+ --method 按调用方法过滤
8814
+ --min-cost <ms> 最小耗时过滤(body.costTime=[ms, 999999])
8815
+ (无 --profiler) 历史参数已移除映射——上游 enableProfiler 是"仅返回有 CPU 采样
8816
+ 记录"的过滤器,实测任何组合均空集。CPU 热点走 dump-list。
8817
+ 示例:
8818
+ bfi ymc slowcall -t 1d --trace-id abc -e core3 # 链路分层分解(跨服务根因链必用)
8819
+ bfi ymc slowcall -t 4w --app yonbip-scm-pu -e pre # 服务慢调用趋势`,
8820
+ exceptions: `bfi ymc exceptions -t <time> -e <env> [--app <appCode>] [--json]
8821
+ 异常聚合(exceptionHistory)。--app 传裸微服务名(appId 需 <env>-<appCode> 格式,传裸名上游才查得到)。`,
8822
+ alerts: `bfi ymc alerts -t <time> -e <env> [--level error|warn|info] [--page-size N] [--json]
8823
+ 告警明细。--page-size 默认 10;--level 可省。`,
8824
+ "url-stats": `bfi ymc url-stats </uri> -t <time> -e <env> --app <service_name> [--json]
8825
+ URL 性能统计(ingress 访问日志聚合):QPS/avg/P50/P90/P95/P99/状态码分布/慢请求Top10(含 traceId)。
8826
+ 参数:
8827
+ --app <service_name> ⚠️ 必带(诊断纪律):共享路径 URL(/bill/* 等通用单据接口)全平台
8828
+ 每个业务域都有——不限定服务=全平台混合假面。传裸 appCode 即可
8829
+ (服务端自适应环境前缀 wildcard,如 online-dc-core3-<app>-out)
8830
+ 示例:
8831
+ bfi ymc url-stats /bill/unaudit -t 1d -e core3 --app yonbip-scm-scmmp`,
8832
+ ingress: `bfi ymc ingress --trace-id <id> | --uri </path> [-t <time>] -e <env> [--exact] [--app <service_name>] [--size N] [--json]
8833
+ ingress 访问日志明细(网关视角:request_time vs upstream_response_time/状态码/pod/租户)。
8834
+ 参数:
8835
+ --trace-id 按链路查(时间可省,默认 30d)
8836
+ --uri 按 URI 查(默认子串匹配,--exact 精确;time 必填)
8837
+ --app service_name 过滤(uri 模式必带,裸 appCode 自适应前缀)
8838
+ --size N 返回条数(默认 100,最大 5000)`,
8839
+ "dump-list": `bfi ymc dump-list -t <time> -e <env> [--app <appCode>] [--type profiler|heap|...] [--json]
8840
+ 诊断文件列表(火焰图/JVM dump;CPU 热点取数的正确入口)。`,
8841
+ cases: `bfi ymc cases <search|show|upload|index> [参数]
8842
+ KEDB 案例库(内部诊断先验第一源)。
8843
+ search [--type <t>] [--finger "<指纹>"] [--service <svc>] [--kw "<关键词>"] [--limit N]
8844
+ show <case_id>
8845
+ upload --file <报告.html> [--type slow-req] [--finger "词1,词2"] [--env] [--service]
8846
+ index 案例库索引重建`,
8847
+ wiki: `bfi ymc wiki <search|show> [参数]
8848
+ 知识库统一检索(confluence + jira + 文库 ecfile 三源——诊断先验的外部源,勿只查其一)。
8849
+ search "<关键词>" [--source confluence|jira|ecfile] [--limit N] [--titles]
8850
+ show <id> [--source wiki|uap-wiki|gfwiki|gfjira|ecfile]
8851
+ 示例:
8852
+ bfi ymc wiki search "/bill/unaudit 超时" # 三源全查(默认)
8853
+ bfi ymc wiki show BIP-BUG-00157126 --source gfjira`,
8854
+ callstat: `bfi ymc callstat -t <time> -e <env> [--page N] [--page-size N] [--json]
8855
+ 慢调用统计版(warningCallstat 聚合视图;体检/趋势用)。--app 经 --data 传(见 call)。`,
8856
+ sqlstat: `bfi ymc sqlstat -t <time> -e <env> [--page N] [--page-size N] [--json]
8857
+ 慢 SQL 统计版(warningSqlstat 聚合视图)。`,
8858
+ events: `bfi ymc events -e <env> [--type <类型>] [--start-time <s>] [--end-time <e>] [--app <appCode>] [--pipeline <code>] [--histogram] [--json]
8859
+ 集群事件明细/直方图。`,
8860
+ "log-url": `bfi ymc log-url -e <env> [--type ingressLog|appLog|instanceLog] [--app <appId>] [--filter-app <ids>]
8861
+ 生成 SLS 日志直通 URL(浏览器直接打开的访问/业务/实例日志页签)。`,
8862
+ "sample-list": `bfi ymc sample-list -t <time> -e <env> [--json]
8863
+ 采样数据列表。`,
8864
+ rca: `bfi ymc rca -e <env> [-t <time> | --start-time <s> --end-time <e>] [--app <appId>] [--app-code <appCode>] [--json]
8865
+ YMC 官方智能诊断 RCA 全报告。⚠️ 长耗时(实测约 300s),上游需 podId 等参数,失败多因参数不足;客户端超时已设 300s。`,
8866
+ "bpr-download": `bfi ymc bpr-download <pk> -e <env> [-o <输出文件>]
8867
+ BPR 报告 HTML 下载(文件流写盘)。pk 来源:bfi ymc call bpr/list --data '{"page":1,"pageSize":10}' -e <env>。
8868
+ 输出默认 bpr_<pk前16>.html。`,
8869
+ call: `bfi ymc call <route> -e <env> [--data '<json>'] [-t <time> | --start-time <s> --end-time <e>] [--timeout <秒>] [--json]
8870
+ 通用透传:覆盖全部注册表端点(warningCall/warningSql/exceptionHistory/bpr/list/alert/detail/tool/list/app/history/environment/uba/query ...)。
8871
+ 完整清单与参数声明:curl http://172.20.35.178:8901/openapi.json 或 /docs(Swagger UI)。
8872
+ 时间参数(-t/--start-time/--end-time)由服务端按端点 time_fields 展开为 beginTs/endTs 等。
8873
+ 示例:
8874
+ bfi ymc call warningCallstat --data '{"curAppCode":"yonbip-scm-scmmp","page":1,"pageSize":10}' -e core3 -t 1h
8875
+ bfi ymc call app/history --data '{"appId":"daily-x","queryType":"podCpuUsage"}' -e daily -t 1d`
8876
+ };
8877
+ function printCommandHelp(cmd) {
8878
+ const h = HELP[cmd];
8879
+ if (h) console.log(h);
8880
+ else {
8881
+ console.error(`未知子命令:${cmd}`);
8882
+ printHelp();
8883
+ process.exit(1);
8884
+ }
8885
+ }
8735
8886
  function printHelp() {
8736
8887
  console.log(`
8737
8888
  bfi ymc — 观测查询(v2 API:YMC/Kibana/SLS/trace)
@@ -8761,8 +8912,11 @@ bfi ymc — 观测查询(v2 API:YMC/Kibana/SLS/trace)
8761
8912
  -t, --time 时间简写(10m/2h/3d/4w = 过去 N 单位到现在)
8762
8913
  --start-time <t> 开始时间 yyyy-MM-dd HH:mm:ss(与 -t 二选一;只传它=到当前)
8763
8914
  --end-time <t> 结束时间(须与 --start-time 同传)
8764
- --json 输出原始 JSON
8915
+ --json 输出原始 JSON(完整信封 {code,msg,data})
8916
+ --dry-run 只打印将发送的 method/URL/body,不实际请求(无需登录)
8765
8917
  --token 覆盖已登录 token(默认用 bfi auth login 的)
8918
+
8919
+ 详细帮助:bfi ymc help <命令>(或 bfi ymc <命令> -h)——含参数说明/默认值/示例。
8766
8920
  `);
8767
8921
  }
8768
8922
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,282 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { z } from "zod";
4
+ import mysql from "mysql2/promise";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import os from "node:os";
8
+ const SEMICOLON_SPLIT = /;\s*(?=(?:[^']*'[^']*')*[^']*$)/;
9
+ const FIRST_WORD = /[a-zA-Z_]+/;
10
+ function no(reason) {
11
+ return {
12
+ ok: false,
13
+ kind: "query",
14
+ reason
15
+ };
16
+ }
17
+ function stripComments(sql) {
18
+ let out = "";
19
+ let i = 0;
20
+ const len = sql.length;
21
+ while (i < len) {
22
+ const c = sql[i];
23
+ if (c === "'") {
24
+ out += c;
25
+ i++;
26
+ while (i < len) {
27
+ out += sql[i];
28
+ if (sql[i] === "'") {
29
+ if (i + 1 < len && sql[i + 1] === "'") {
30
+ out += "'";
31
+ i += 2;
32
+ continue;
33
+ }
34
+ i++;
35
+ break;
36
+ }
37
+ i++;
38
+ }
39
+ } else if (c === "-" && sql[i + 1] === "-") while (i < len && sql[i] !== "\n") i++;
40
+ else if (c === "#") while (i < len && sql[i] !== "\n") i++;
41
+ else if (c === "/" && sql[i + 1] === "*") {
42
+ i += 2;
43
+ while (i + 1 < len && !(sql[i] === "*" && sql[i + 1] === "/")) i++;
44
+ i = Math.min(i + 2, len);
45
+ } else {
46
+ out += c;
47
+ i++;
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+ function firstKeyword(sql) {
53
+ const m = sql.match(FIRST_WORD);
54
+ return m ? m[0].toLowerCase() : "";
55
+ }
56
+ function checkGate(rawSql) {
57
+ if (!rawSql || !rawSql.trim()) return no("SQL 为空");
58
+ const stripped = stripComments(rawSql);
59
+ if (!stripped.trim()) return no("SQL 为空(仅注释)");
60
+ const stmts = stripped.trim().split(SEMICOLON_SPLIT).map((s) => s.trim()).filter(Boolean);
61
+ if (stmts.length > 1) return no(`仅支持单语句,实际 ${stmts.length} 条`);
62
+ const word = firstKeyword(stmts[0]);
63
+ switch (word) {
64
+ case "select":
65
+ case "with": return {
66
+ ok: true,
67
+ kind: "query"
68
+ };
69
+ case "explain": {
70
+ const rest = stmts[0].replace(/^explain\s+/i, "");
71
+ if (/^analyze/i.test(rest)) return no("EXPLAIN ANALYZE 会真实执行语句,拒绝");
72
+ const target = firstKeyword(rest);
73
+ if (target === "select" || target === "with") return {
74
+ ok: true,
75
+ kind: "explain"
76
+ };
77
+ return no("EXPLAIN 仅支持 SELECT/WITH 目标语句");
78
+ }
79
+ default: return no(`本地直连仅允许 SELECT/WITH/EXPLAIN,收到: ${(word || "空").toUpperCase()}`);
80
+ }
81
+ }
82
+ const DEFAULT_MAX_ROWS = 200;
83
+ const MAX_ROWS_LIMIT = 2e3;
84
+ const QUERY_TIMEOUT_MS = 3e4;
85
+ function errMessage(e) {
86
+ const msg = e instanceof Error ? e.message : String(e);
87
+ return msg.length > 300 ? `${msg.slice(0, 300)}...` : msg;
88
+ }
89
+ async function directExecute(dsn, sql, maxRows = DEFAULT_MAX_ROWS) {
90
+ const gate = checkGate(sql);
91
+ if (!gate.ok) return {
92
+ ok: false,
93
+ error_kind: "unsupported_sql",
94
+ message: gate.reason
95
+ };
96
+ const limit = Math.min(Math.max(maxRows, 1), MAX_ROWS_LIMIT);
97
+ let conn;
98
+ try {
99
+ conn = await mysql.createConnection({
100
+ uri: dsn,
101
+ connectTimeout: 8e3
102
+ });
103
+ } catch (e) {
104
+ return {
105
+ ok: false,
106
+ error_kind: "connection_failed",
107
+ message: `连接失败: ${errMessage(e)}`
108
+ };
109
+ }
110
+ try {
111
+ await conn.query("SELECT 1");
112
+ const started = Date.now();
113
+ const [rows, fields] = await conn.query({
114
+ sql,
115
+ timeout: QUERY_TIMEOUT_MS,
116
+ rowsAsArray: true
117
+ });
118
+ const arr = rows;
119
+ const truncated = arr.length > limit;
120
+ return {
121
+ ok: true,
122
+ mode: "direct(本地直连,词法级闸门)",
123
+ columns: fields?.map((f) => f.name) ?? [],
124
+ rows: arr.slice(0, limit),
125
+ row_count: Math.min(arr.length, limit),
126
+ truncated,
127
+ elapsed_ms: Date.now() - started
128
+ };
129
+ } catch (e) {
130
+ return {
131
+ ok: false,
132
+ error_kind: "execute_failed",
133
+ message: errMessage(e)
134
+ };
135
+ } finally {
136
+ await conn.end().catch(() => {});
137
+ }
138
+ }
139
+ const GLOBAL_CONFIG_DIR = path.join(os.homedir(), ".bfi");
140
+ const GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, "config.json");
141
+ path.join(".bfi", "lock.json");
142
+ function loadGlobalConfig() {
143
+ try {
144
+ if (fs.existsSync(GLOBAL_CONFIG_FILE)) {
145
+ const content = fs.readFileSync(GLOBAL_CONFIG_FILE, "utf8");
146
+ return JSON.parse(content);
147
+ }
148
+ } catch (error) {
149
+ console.warn(`Warning: Failed to load global config: ${error.message}`);
150
+ }
151
+ return null;
152
+ }
153
+ function resolveApiBase() {
154
+ return process.env.BFI_API_BASE || "http://172.20.35.178:8901";
155
+ }
156
+ function resolveToken() {
157
+ if (process.env.BFI_TOKEN) return process.env.BFI_TOKEN;
158
+ return loadGlobalConfig()?.token ?? null;
159
+ }
160
+ var RestError = class extends Error {
161
+ errorKind;
162
+ constructor(errorKind, message) {
163
+ super(message);
164
+ this.errorKind = errorKind;
165
+ }
166
+ };
167
+ async function post(path, payload, fetchImpl = fetch) {
168
+ const token = resolveToken();
169
+ if (!token) throw new RestError("unauthorized", "未找到 bfi token:请先执行 `bfi auth login --token <sk-token>`(token 获取地址 https://bfi.yyuap.com/user/profile),或设置 BFI_TOKEN 环境变量");
170
+ let resp;
171
+ try {
172
+ resp = await fetchImpl(resolveApiBase() + path, {
173
+ method: "POST",
174
+ headers: {
175
+ "Content-Type": "application/json",
176
+ Authorization: `Bearer ${token}`
177
+ },
178
+ body: JSON.stringify(payload)
179
+ });
180
+ } catch (e) {
181
+ throw new RestError("upstream_unreachable", `bfi-api(${resolveApiBase()})不可达:${e instanceof Error ? e.message : String(e)}`);
182
+ }
183
+ if (resp.status === 401) throw new RestError("unauthorized", "v2 token 无效或过期:请重新 `bfi auth login --token <token>`");
184
+ const body = await resp.json().catch(() => ({}));
185
+ if (body.code === 0) return body.data;
186
+ throw new RestError(body.data?.error_kind || "upstream_error", body.msg || "bfi-api 返回错误");
187
+ }
188
+ function listEnvs(keyword, envType, fetchImpl) {
189
+ return post("/bficli/api/v2/db/envs", {
190
+ keyword,
191
+ env_type: envType,
192
+ enabled_only: true
193
+ }, fetchImpl);
194
+ }
195
+ function execSql(envCode, sql, maxRows, fetchImpl) {
196
+ return post("/bficli/api/v2/db/exec", {
197
+ env_code: envCode,
198
+ sql,
199
+ max_rows: maxRows
200
+ }, fetchImpl);
201
+ }
202
+ function explainSql(envCode, sql, fetchImpl) {
203
+ return post("/bficli/api/v2/db/explain", {
204
+ env_code: envCode,
205
+ sql
206
+ }, fetchImpl);
207
+ }
208
+ function textResult(payload, isError = false) {
209
+ return {
210
+ content: [{
211
+ type: "text",
212
+ text: JSON.stringify(payload, null, 2)
213
+ }],
214
+ isError
215
+ };
216
+ }
217
+ function wrap(fn) {
218
+ return async (args) => {
219
+ try {
220
+ return textResult(await fn(args));
221
+ } catch (e) {
222
+ return textResult({
223
+ ok: false,
224
+ error_kind: e instanceof RestError ? e.errorKind : "internal_error",
225
+ message: e instanceof Error ? e.message : String(e)
226
+ }, true);
227
+ }
228
+ };
229
+ }
230
+ function buildServer() {
231
+ const server = new McpServer({
232
+ name: "bfi-db",
233
+ version: "0.1.0"
234
+ }, { instructions: [
235
+ "数据库取证通道(只读):查询环境列表 → EXPLAIN 执行计划(优先)→ 必要时小行数 SELECT 采样。",
236
+ "DDL/DML 默认被环境权限位拒绝;db_direct 为用户自备连接的本地直连,词法级闸门仅放行 SELECT/WITH/EXPLAIN,结果无审计,写报告须标注\"本地直连取证\"。",
237
+ "执行前先用 db_list_envs 锁定 env_code(对应诊断的 appCode 先行铁律)。"
238
+ ].join("\n") });
239
+ server.registerTool("db_list_envs", {
240
+ description: "获取可查询的数据库环境列表(env_code/env_name/db_type/datacenter/权限位 allow_ddl·dml·query·explain)。不含密码。执行任何 SQL 前必须先用本工具锁定 env_code。",
241
+ inputSchema: {
242
+ keyword: z.string().optional().describe("模糊关键字,匹配 env_code/env_name/datacenter(如 core3、商开)"),
243
+ env_type: z.enum([
244
+ "private",
245
+ "dev",
246
+ "prod"
247
+ ]).optional().describe("环境类型过滤")
248
+ }
249
+ }, wrap((args) => listEnvs(args.keyword, args.env_type)));
250
+ server.registerTool("db_execute", {
251
+ description: "在指定环境执行单条 SQL(只读取证)。仅 SELECT/WITH/SHOW/DESC/EXPLAIN;DML/DDL 需环境显式授权位否则 permission_denied。行数上限默认 200。非 mysql 库自动翻译(mysql 语法输入即可)。诊断场景优先 db_explain。",
252
+ inputSchema: {
253
+ env_code: z.string().describe("环境编码,来自 db_list_envs"),
254
+ sql: z.string().describe("单条 SQL(mysql 语法)"),
255
+ max_rows: z.number().int().min(1).max(2e3).optional().describe("行数上限,默认 200")
256
+ }
257
+ }, wrap((args) => execSql(args.env_code, args.sql, args.max_rows)));
258
+ server.registerTool("db_explain", {
259
+ description: "获取执行计划(慢 SQL 实证首选)。目标语句必须为 SELECT/WITH;gscm 按 db_type 组装 EXPLAIN(oracle 两步 EXPLAIN PLAN + DBMS_XPLAN)。结果为计划行,EXPLAIN 结果进报告须附原始计划行。",
260
+ inputSchema: {
261
+ env_code: z.string().describe("环境编码,来自 db_list_envs"),
262
+ sql: z.string().describe("目标 SELECT/WITH 语句(mysql 语法)")
263
+ }
264
+ }, wrap((args) => explainSql(args.env_code, args.sql)));
265
+ server.registerTool("db_direct", {
266
+ description: "用户自备连接串的本地直连(不经服务端,无审计)。词法级闸门仅放行单条 SELECT/WITH/EXPLAIN(EXPLAIN ANALYZE 拒绝),执行前连接预检。仅当用户明确提供连接串且环境通道不可达时使用;报告引用本工具结果必须标注\"本地直连取证\"。",
267
+ inputSchema: {
268
+ dsn: z.string().describe("连接串 mysql://user:pwd@host:3306/db(密码仅内存持有)"),
269
+ sql: z.string().describe("单条 SELECT/WITH/EXPLAIN"),
270
+ max_rows: z.number().int().min(1).max(2e3).optional().describe("行数上限,默认 200")
271
+ }
272
+ }, wrap((args) => directExecute(args.dsn, args.sql, args.max_rows)));
273
+ return server;
274
+ }
275
+ async function main() {
276
+ await buildServer().connect(new StdioServerTransport());
277
+ }
278
+ main().catch((e) => {
279
+ console.error("[bfi-db-mcp] 启动失败:", e);
280
+ process.exit(1);
281
+ });
282
+ export {};
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "bfi-cli",
3
- "version": "0.1.69",
3
+ "version": "0.1.72",
4
4
  "description": "用友大业财BFI统一命令行工具,支持技能,技能包,元数据,UI元数据等",
5
5
  "type": "module",
6
6
  "bin": {
7
- "bfi": "./bin/bfi.js"
7
+ "bfi": "./bin/bfi.js",
8
+ "bfi-db-mcp": "./bin/bfi-db-mcp.js"
8
9
  },
9
10
  "files": [
10
11
  "dist",
@@ -46,7 +47,10 @@
46
47
  "node": ">=18"
47
48
  },
48
49
  "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.12.0",
49
51
  "@vercel/detect-agent": "^1.2.3",
50
- "yaml": "^2.8.3"
52
+ "mysql2": "^3.14.0",
53
+ "yaml": "^2.8.3",
54
+ "zod": "^3.25.0"
51
55
  }
52
56
  }