basedoc-dameng-mcp 0.1.1 → 0.1.2

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 CHANGED
@@ -1,128 +1,201 @@
1
- # dameng-mcp
1
+ # basedoc-dameng-mcp
2
2
 
3
- Read-only MCP server for Dameng (DM8). Built after rejecting `zqeast/dameng-mcp-server` for stdout-leaking the password and offering an unguarded `execute_sql`.
3
+ 面向达梦数据库 (DM8) 的只读 MCP 服务,附带**本地 Markdown 手册**的离线全文检索能力。把"运维问答"和"翻手册"这两件事做成 LLM 直接可调用的工具,让 Claude / 其它 MCP 客户端在一次会话里既能问数据库当前状态,也能查官方文档。
4
4
 
5
- ## Guarantees
5
+ 整套搜索能力**完全离线**——内网部署的达梦客户也能用。
6
6
 
7
- - **Read-only at the application layer.** Only `SELECT` / `WITH` / `EXPLAIN` are accepted. Multi-statement input is rejected. Leading line/block comments are stripped before classification, so `-- foo\nDROP TABLE t` is also rejected.
8
- - **No password fallback.** `DAMENG_PASSWORD` must be set; missing → exit 2. Nothing defaults to `SYSDBA`.
9
- - **stderr-only logging.** stdio's stdout is the JSON-RPC channel — every log line goes through `console.error`.
10
- - **Timeout + row cap.** Every query is bounded by `DAMENG_QUERY_TIMEOUT_MS` (default 10s) and capped at `DAMENG_MAX_ROWS` (default 1000). On timeout the underlying connection is closed.
11
- - **Pinned dependencies.** `dmdb`, `@modelcontextprotocol/sdk`, and `zod` are all locked to exact versions.
7
+ ---
12
8
 
13
- The first line of defense is still **using a low-privilege Dameng account**. The app-layer guard is belt-and-braces; the database account should not have DML/DDL grants in the first place.
9
+ ## 快速开始
14
10
 
15
- ## Provision a read-only DB user
11
+ 接入这个 MCP 需要先准备一个 markdown 手册目录(用作搜索语料)。两条路:
16
12
 
17
- ```sql
18
- CREATE USER MCP_RO IDENTIFIED BY "<strong-password>";
19
- GRANT CONNECT TO MCP_RO;
20
- GRANT SELECT ON <schema>.<table> TO MCP_RO;
21
- -- or grant on a whole schema you trust:
22
- -- GRANT SELECT ANY TABLE TO MCP_RO;
23
- GRANT SELECT ON V$INSTANCE TO MCP_RO;
24
- GRANT SELECT ON V$DATABASE TO MCP_RO;
13
+ ### 路径 A:你已经有 markdown 手册目录
14
+
15
+ 直接跳到下面的 [接入 Claude Code](#接入-claude-code),把 `DAMENG_DOCS_ROOT` 指过去。
16
+
17
+ ### 路径 B:你还没有,从达梦官方文档站抓一份
18
+
19
+ 包内置了一个抓取转换工具(`basedoc-dameng-fetch-docs`),从 https://eco.dameng.com 拉所有手册并自动转 markdown,写到本地目录:
20
+
21
+ ```bash
22
+ npx -y basedoc-dameng-mcp basedoc-dameng-fetch-docs ~/.dameng-docs
23
+ # 或者直接调 bin:
24
+ npx -y -p basedoc-dameng-mcp basedoc-dameng-fetch-docs ~/.dameng-docs
25
+ ```
26
+
27
+ 完成后会得到 22 本手册、~250 个章节、~9 MB 的 markdown 目录,下面像这样:
28
+
29
+ ```
30
+ ~/.dameng-docs/
31
+ ├── README.md
32
+ ├── DM8-系统管理员手册/
33
+ │ ├── system-administrator.md
34
+ │ ├── physical-storage-structure.md
35
+ │ └── ...
36
+ ├── DM8-SQL 语言使用手册/
37
+ ├── DM8-安全管理/
38
+ └── ...
25
39
  ```
26
40
 
27
- ## Build
41
+ 抓取过程默认 5 并发、约 2-3 秒结束。需要联通 `eco.dameng.com`——内网/隔离环境跑不了,请走路径 A 或者在能联网的机器上抓完后再 rsync 进内网。
42
+
43
+ > **关于版权**:抓取下来的内容版权属于**达梦数据库股份有限公司**,仅供个人/团队内部参考使用,**不要公开发布**。
44
+
45
+ ---
46
+
47
+ ## 接入 Claude Code
28
48
 
29
49
  ```bash
30
- npm install
31
- npm run build
50
+ claude mcp add dameng \
51
+ --scope user \
52
+ -e DAMENG_DOCS_ROOT="$HOME/.dameng-docs" \
53
+ -e DAMENG_DOCS_INCLUDE='DM8-*' \
54
+ -e DAMENG_HOST=<达梦主机> \
55
+ -e DAMENG_PORT=5236 \
56
+ -e DAMENG_USER=MCP_RO \
57
+ -e DAMENG_PASSWORD=<密码> \
58
+ -- npx -y basedoc-dameng-mcp
32
59
  ```
33
60
 
34
- ## Modes
61
+ 不需要数据库工具的话,所有 `DAMENG_HOST` / `PORT` / `USER` / `PASSWORD` 都可以省略——只剩文档检索能力(**仅文档模式**)。
35
62
 
36
- The server supports two run modes, picked automatically from env vars:
63
+ ---
37
64
 
38
- - **DB + docs mode** (set both DB credentials and `DAMENG_DOCS_ROOT`): all 8 tools registered.
39
- - **Docs-only mode** (set only `DAMENG_DOCS_ROOT`, no DB credentials): just the 4 docs tools — useful when you want to ship Dameng manuals to an LLM without exposing the database.
40
- - (DB-only mode also works if you set DB creds without `DAMENG_DOCS_ROOT`.)
65
+ ## 启动模式
41
66
 
42
- Server fails fast (exit 2) if neither set is provided.
67
+ 按环境变量自动判定:
43
68
 
44
- ## Configuration (env vars)
69
+ | 给的环境变量 | 启动什么 |
70
+ | --- | --- |
71
+ | 完整 DB 凭据 + `DAMENG_DOCS_ROOT` | 全部 8 个工具 |
72
+ | 只有 DB 凭据 | 只 4 个 DB 工具 |
73
+ | 只有 `DAMENG_DOCS_ROOT` | 只 4 个文档工具——适合"只让 LLM 看手册、不暴露数据库" |
74
+ | 都没有 | exit 2 |
45
75
 
46
- | Variable | Required | Default | Notes |
47
- |---|---|---|---|
48
- | `DAMENG_HOST` | for DB | — | e.g. `dameng.example.com` |
49
- | `DAMENG_PORT` | for DB | — | e.g. `5236` |
50
- | `DAMENG_USER` | for DB | — | low-privilege account |
51
- | `DAMENG_PASSWORD` | for DB | — | no fallback |
52
- | `DAMENG_SCHEMA` | no | — | default schema for `list_tables` / `describe_table` |
53
- | `DAMENG_QUERY_TIMEOUT_MS` | no | `10000` | per-query timeout |
54
- | `DAMENG_MAX_ROWS` | no | `1000` | hard cap on returned rows |
55
- | `DAMENG_POOL_MIN` | no | `1` | |
56
- | `DAMENG_POOL_MAX` | no | `4` | |
57
- | `DAMENG_DOCS_ROOT` | no | — | absolute path to a directory of Markdown manuals. Enables the docs tools. |
58
- | `DAMENG_DOCS_INCLUDE` | no | — | comma-separated top-level dir patterns to include (glob `*` supported). Recommended: `DM8-*` to scope to official manuals only. |
59
- | `DAMENG_DOCS_EXCLUDE` | no | — | comma-separated top-level dir patterns to exclude. `node_modules` is always excluded. |
60
- | `DAMENG_NO_LEGACY_OPENSSL` | no | unset | Set to `1` to skip the auto-relaunch with `--openssl-legacy-provider` (only useful if your dmdb build doesn't need it). |
76
+ ---
61
77
 
62
- ## Tools
78
+ ## 工具
63
79
 
64
- ### Database (require DB connection)
65
- - `query(sql)` — run a single read-only statement.
66
- - `list_tables(schema?)` — tables in a schema (defaults to `DAMENG_SCHEMA` or current user).
67
- - `describe_table(table, schema?)` — columns + types for one table.
68
- - `instance_status()` — `V$INSTANCE` + `V$DATABASE` snapshot.
80
+ ### 数据库(需要 DB 凭据)
69
81
 
70
- ### Docs (require `DAMENG_DOCS_ROOT`)
71
- - `list_manuals()` discover what manuals exist; each entry has `fileCount` and a short index summary (from `00-目录索引.md` or `README.md`).
72
- - `list_sections(manual)` list `.md` chapters in one manual with their H1/H2 headings (with line numbers).
73
- - `read_section(file, heading?, maxBytes?)` read a chapter or a single heading-bounded section. Use this to pull full context after a `lookup_docs` hit.
74
- - `lookup_docs(query, manual?, regex?, maxMatches?)` keyword/regex search. Results are **grouped by section** (file + nearest heading), each match has line ± 1 line of context, sections sorted by match count descending.
82
+ | 工具 | 说明 |
83
+ | --- | --- |
84
+ | `query(sql)` | 执行单条只读 SQL |
85
+ | `list_tables(schema?)` | 列某 schema 下的表 |
86
+ | `describe_table(table, schema?)` | 列字段名 / 类型 / 是否可空 / 默认值 |
87
+ | `instance_status()` | `V$INSTANCE` + `V$DATABASE` 当前快照 |
75
88
 
76
- **Recommended workflow** for the LLM client:
77
- 1. `list_manuals()` → know what's available
78
- 2. `lookup_docs("keyword", manual: "DM8-...")` → find candidate sections
79
- 3. `read_section(file, heading)` → pull the full passage
80
- 4. Synthesize the answer for the user
89
+ ### 文档(需要 `DAMENG_DOCS_ROOT`)
81
90
 
82
- ### Safety
83
- - `list_tables` / `describe_table` build their SQL internally and only accept identifier-shaped input (`[A-Za-z_][A-Za-z0-9_]*`).
84
- - All docs tools only read `.md` files under `DAMENG_DOCS_ROOT`. `manual` and `file` parameters reject `..`, leading `.`, leading `/`, and other unsafe segments — no path traversal. `node_modules` directories are never walked.
91
+ | 工具 | 说明 |
92
+ | --- | --- |
93
+ | `list_manuals()` | 根目录下都有哪些手册(用 `00-目录索引.md` / `README.md` 当摘要) |
94
+ | `list_sections(manual)` | 某本手册里有哪些 .md 章节,每个带 H1 / H2 标题 + 行号 |
95
+ | `read_section(file, heading?, maxBytes?)` | 读整个章节文件;带 heading 则只切出对应小节 |
96
+ | `lookup_docs(query, manual?, regex?, maxMatches?)` | 全文检索;按 (文件 + 最近 heading) 分组,每条带 ±1 行上下文,按命中数倒序 |
85
97
 
86
- ## Register with Claude Code
98
+ ### LLM 客户端的推荐工作流
87
99
 
88
- ```bash
89
- claude mcp add dameng-prod \
90
- -e DAMENG_HOST=<your-dameng-host> \
91
- -e DAMENG_PORT=5236 \
92
- -e DAMENG_USER=MCP_RO \
93
- -e DAMENG_PASSWORD=<password> \
94
- -e DAMENG_SCHEMA=<your-schema> \
95
- -e DAMENG_DOCS_ROOT=/path/to/dameng-docs \
96
- -e DAMENG_DOCS_INCLUDE='DM8-*' \
97
- -- npx -y basedoc-dameng-mcp
100
+ ```
101
+ list_manuals()
102
+
103
+ lookup_docs(query, manual="DM8-...")
104
+
105
+ read_section(file, heading="...")
106
+
107
+ (合成回答)
98
108
  ```
99
109
 
100
- Or, if you cloned the repo and want to run from source:
110
+ `list_tables` `describe_table` 内部生成 SQL,对入参做严格的标识符正则校验(`[A-Za-z_][A-Za-z0-9_]*`),不会拼接出注入。
101
111
 
102
- ```bash
103
- -- node /path/to/basedoc-dameng-mcp/dist/server.js
112
+ ---
113
+
114
+ ## 取舍清单
115
+
116
+ 不该让 LLM 做的事,挡在工具调用之前:
117
+
118
+ - SQL 入参只放 `SELECT` / `WITH` / `EXPLAIN`,多语句 / 注释绕过 / `DROP` 之类的关键字一律拒绝
119
+ - `DAMENG_PASSWORD` 不写也不补——缺失就直接退出 (exit 2),不存在 SYSDBA 兜底
120
+ - stdout 留给 JSON-RPC,所有日志都走 stderr
121
+ - 每个查询带超时 + 行数硬上限
122
+ - 文档相关的工具不能跨出 `DAMENG_DOCS_ROOT` 边界——`..`、绝对路径、`node_modules` 全部拦截
123
+ - 关键依赖(`@modelcontextprotocol/sdk`、`dmdb`、`zod`、`turndown`)锁死精确版本
124
+
125
+ ---
126
+
127
+ ## 环境变量
128
+
129
+ | 变量 | 何时必填 | 默认 | 说明 |
130
+ | --- | --- | --- | --- |
131
+ | `DAMENG_HOST` | DB 模式 | — | 达梦服务器地址 |
132
+ | `DAMENG_PORT` | DB 模式 | — | 比如 `5236` |
133
+ | `DAMENG_USER` | DB 模式 | — | 推荐建低权账号 |
134
+ | `DAMENG_PASSWORD` | DB 模式 | — | 没有任何兜底 |
135
+ | `DAMENG_SCHEMA` | 否 | — | `list_tables` / `describe_table` 默认查的 schema |
136
+ | `DAMENG_QUERY_TIMEOUT_MS` | 否 | `10000` | 单次查询超时 |
137
+ | `DAMENG_MAX_ROWS` | 否 | `1000` | 单次返回行数硬上限 |
138
+ | `DAMENG_POOL_MIN` / `DAMENG_POOL_MAX` | 否 | `1` / `4` | 连接池上下限 |
139
+ | `DAMENG_DOCS_ROOT` | 文档模式 | — | 含 markdown 的目录的**绝对路径** |
140
+ | `DAMENG_DOCS_INCLUDE` | 否 | — | 顶层目录白名单,逗号分隔,支持 `*` 通配,例如 `DM8-*` |
141
+ | `DAMENG_DOCS_EXCLUDE` | 否 | — | 顶层目录黑名单。`node_modules` 永远硬排,无需手动加 |
142
+ | `DAMENG_NO_LEGACY_OPENSSL` | 否 | 不设 | 设 `1` 跳过下文那个自重启 |
143
+
144
+ ---
145
+
146
+ ## 准备一个低权数据库账号
147
+
148
+ 直接给 SYSDBA 跑也能用——SQL 守卫已经限死只读——但纵深防御原则下建议:
149
+
150
+ ```sql
151
+ CREATE USER MCP_RO IDENTIFIED BY "<强密码>";
152
+ GRANT SELECT ANY TABLE TO MCP_RO;
153
+ GRANT SELECT ON V$INSTANCE TO MCP_RO;
154
+ GRANT SELECT ON V$DATABASE TO MCP_RO;
104
155
  ```
105
156
 
106
- Register one MCP server per Dameng instance. If the same box hosts multiple instances on different ports, use distinct names like `dameng-prod-a` / `dameng-prod-b`.
157
+ 注:达梦中 `CREATE USER` 后默认就有 session 权限,无需 `GRANT CONNECT`(这条会报语法错)。
107
158
 
108
- ## Tests
159
+ ---
109
160
 
110
- ```bash
161
+ ## 二进制为什么会自重启
162
+
163
+ `dmdb` 驱动握手阶段使用了 OpenSSL 3 在 Node 17+ 默认禁用的算法。不带 `--openssl-legacy-provider` 启动 Node 时会报 `[6071] 消息加密失败` / `error:0308010C`。本包的 bin 入口检测到没带这个 flag 就自己 spawn 一次新进程加上,对调用者透明。代价是约 50ms 的额外启动。如果你的 dmdb 版本不需要,设 `DAMENG_NO_LEGACY_OPENSSL=1` 跳过。
164
+
165
+ ---
166
+
167
+ ## 测试
168
+
169
+ ```
111
170
  npm test
112
171
  ```
113
172
 
114
- Tests cover the SQL guard (rejection rules, comment stripping, multi-statement detection), the config loader (required vars, no password fallback, type validation, docs-root + include/exclude validation), the timeout helper, and the docs layer (`searchDocs` grouping/ranking/context, `listManuals`, `listSections`, `readSection` heading extraction + path-traversal safety). The dmdb integration is not unit-tested; verify it by pointing at a real instance.
173
+ 117 个用例,覆盖:
174
+
175
+ - SQL 守卫的关键字白名单 / 注释剥离 / 多语句检测
176
+ - 配置加载,包括无 SYSDBA 兜底的明确断言、include/exclude 解析
177
+ - 超时 helper
178
+ - 4 个文档函数:搜索分组与排名、目录列出、章节按 heading 切片读取、路径越权防护
179
+ - HTML→MD 抓取链路:sidebar 解析、article 抽取、turndown 包装
180
+
181
+ dmdb 与 MCP 协议握手部分没有单元测试,需要指向真实达梦实例验证。
182
+
183
+ ---
184
+
185
+ ## 故意不做的能力
186
+
187
+ - 不暴露 DML / DDL / 系统过程调用——写入操作请直接走 dmctl 或 JDBC
188
+ - 不开 HTTP transport——只 stdio,没有可被外部访问的网络面
189
+ - 启动时不做 schema 自检——按需查系统视图,避免不必要的握手成本
115
190
 
116
- ## What is NOT here, by design
191
+ ---
117
192
 
118
- - No `execute_sql` / no DML / no DDL. If you need writes, write them directly with `dmctl`/JDBC, not through an LLM.
119
- - No HTTP transport. stdio only — no inbound network surface.
120
- - No automatic schema introspection on startup. Tools query system views on demand.
193
+ ## 已知传递依赖告警
121
194
 
122
- ## Why does the binary self-relaunch?
195
+ `npm audit` 会标记 `@modelcontextprotocol/sdk` HTTP transport 链(`express-rate-limit` → `ip-address`)以及开发环境 Vitest / Vite 链上的几个中等漏洞。本服务只跑 stdio transport,HTTP 那条链不会被加载;Vitest 是开发依赖不进入运行时。等上游升级后会自动消失,不会用 `npm audit fix --force` 强行覆盖。
123
196
 
124
- `dmdb`'s login handshake uses encryption algorithms OpenSSL 3 disabled by default in Node 17+. Without `--openssl-legacy-provider` you get `[6071] 消息加密失败` / `error:0308010C`. Rather than asking every user to set `NODE_OPTIONS=--openssl-legacy-provider`, the bin re-execs itself with the flag on first start. Cost is one extra ~50 ms Node startup. Opt out with `DAMENG_NO_LEGACY_OPENSSL=1`.
197
+ ---
125
198
 
126
- ## Known transitive vulnerabilities
199
+ ## 许可证
127
200
 
128
- `npm audit` flags issues in `@modelcontextprotocol/sdk`'s HTTP transport chain (`express-rate-limit` → `ip-address`) and in the dev-only Vitest/Vite chain. The stdio server we ship doesn't load the HTTP transport, and Vitest isn't part of the runtime. These will clear when upstream bumps; we don't paper over them with `npm audit fix --force`.
201
+ MIT。详见 [LICENSE](./LICENSE)
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { fetchAndConvertAll } from "./fetch-docs.js";
4
+ const DEFAULT_SOURCE = "https://eco.dameng.com/document/dm/zh-cn/pm/";
5
+ function usage() {
6
+ process.stderr.write([
7
+ "用法: basedoc-dameng-fetch-docs <目标目录> [--source <URL>] [--concurrency <N>]",
8
+ "",
9
+ " 从达梦官方在线文档抓取所有手册并转换为 Markdown,写入指定目录。",
10
+ " 目标目录可以直接当作 DAMENG_DOCS_ROOT 给 basedoc-dameng-mcp 使用。",
11
+ "",
12
+ "选项:",
13
+ " --source <URL> 达梦文档站点的索引页 URL",
14
+ ` 默认: ${DEFAULT_SOURCE}`,
15
+ " --concurrency <N> 并发下载数 (默认 5)",
16
+ " -h, --help 显示帮助",
17
+ "",
18
+ "示例:",
19
+ " basedoc-dameng-fetch-docs ~/.dameng-docs",
20
+ " npx -y basedoc-dameng-mcp/dist/bin-fetch.js ~/.dameng-docs",
21
+ "",
22
+ "注意: 抓取到的内容版权属于达梦数据库股份有限公司。仅供个人或团队",
23
+ " 内部使用,不要公开再发布。",
24
+ "",
25
+ ].join("\n"));
26
+ process.exit(2);
27
+ }
28
+ function parseArgs(argv) {
29
+ let target;
30
+ let source = DEFAULT_SOURCE;
31
+ let concurrency = 5;
32
+ for (let i = 0; i < argv.length; i++) {
33
+ const a = argv[i];
34
+ if (a === "-h" || a === "--help")
35
+ usage();
36
+ else if (a === "--source")
37
+ source = argv[++i] ?? usage();
38
+ else if (a === "--concurrency")
39
+ concurrency = parseInt(argv[++i] ?? "", 10) || usage();
40
+ else if (a.startsWith("-")) {
41
+ process.stderr.write(`未知选项: ${a}\n`);
42
+ usage();
43
+ }
44
+ else if (!target)
45
+ target = a;
46
+ else {
47
+ process.stderr.write(`只支持一个目标目录参数,已经是 ${target}\n`);
48
+ usage();
49
+ }
50
+ }
51
+ if (!target)
52
+ usage();
53
+ if (concurrency < 1 || concurrency > 20) {
54
+ process.stderr.write(`concurrency 取值必须在 1–20 之间,得到 ${concurrency}\n`);
55
+ process.exit(2);
56
+ }
57
+ return { target: resolve(target), source, concurrency };
58
+ }
59
+ async function main() {
60
+ const args = parseArgs(process.argv.slice(2));
61
+ process.stderr.write(`目标目录: ${args.target}\n`);
62
+ process.stderr.write(`源站点: ${args.source}\n`);
63
+ process.stderr.write(`并发: ${args.concurrency}\n\n`);
64
+ const t0 = Date.now();
65
+ const result = await fetchAndConvertAll({
66
+ source: args.source,
67
+ targetDir: args.target,
68
+ concurrency: args.concurrency,
69
+ onProgress: (m) => process.stderr.write(m + "\n"),
70
+ });
71
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
72
+ process.stderr.write(`\n完成: ${result.manualCount} 本手册, ${result.chapterCount} 个章节, ${elapsed}s\n`);
73
+ if (result.failures.length > 0) {
74
+ process.stderr.write(`\n失败 ${result.failures.length} 个章节:\n`);
75
+ for (const f of result.failures) {
76
+ process.stderr.write(` ${f.url}: ${f.error}\n`);
77
+ }
78
+ process.stderr.write(`\n可以重新运行同一条命令来重试 (已成功的章节会被覆盖)。\n`);
79
+ process.exit(1);
80
+ }
81
+ process.stderr.write(`\n下一步: 把 ${args.target} 当 DAMENG_DOCS_ROOT 用,例如:\n` +
82
+ ` claude mcp add dameng \\\n` +
83
+ ` --scope user \\\n` +
84
+ ` -e DAMENG_DOCS_ROOT="${args.target}" \\\n` +
85
+ ` -e DAMENG_DOCS_INCLUDE='DM8-*' \\\n` +
86
+ ` -- npx -y basedoc-dameng-mcp\n`);
87
+ }
88
+ main().catch((e) => {
89
+ process.stderr.write(`[fetch-docs] 错误: ${e instanceof Error ? e.message : String(e)}\n`);
90
+ process.exit(1);
91
+ });
92
+ //# sourceMappingURL=bin-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin-fetch.js","sourceRoot":"","sources":["../src/bin-fetch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,175 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import TurndownService from "turndown";
4
+ const ICONFONT_RANGE = /[-]/g;
5
+ const HTML_ICONFONT_ENTITY = /&#x[eE][0-9a-fA-F]{3};/g;
6
+ export function normalizeManualName(raw) {
7
+ return raw
8
+ .replace(HTML_ICONFONT_ENTITY, "")
9
+ .replace(ICONFONT_RANGE, "")
10
+ .replace(/\s+/g, " ")
11
+ .trim()
12
+ .replace(/^DM8\s+/, "DM8-");
13
+ }
14
+ export function parseManualsFromSidebar(html) {
15
+ // Each manual is rendered as `<div class="left-sidebar-link-group top" ...>`
16
+ // The literal string `left-sidebar-link-group` also appears inside <script>
17
+ // blocks for the toggle animation; we anchor to the `top` modifier so we
18
+ // only match the actual HTML wrappers.
19
+ const blockRe = /<div\s+class="left-sidebar-link-group top"[^>]*>/g;
20
+ const positions = [];
21
+ let mm;
22
+ while ((mm = blockRe.exec(html)) !== null)
23
+ positions.push(mm.index);
24
+ const out = [];
25
+ for (let i = 0; i < positions.length; i++) {
26
+ const start = positions[i];
27
+ const end = i + 1 < positions.length ? positions[i + 1] : html.length;
28
+ let block = html.slice(start, end);
29
+ // Bound to the closing of this group's container — the `bar-border`
30
+ // separator that always follows a finished group.
31
+ const closeAt = block.search(/<div\s+class="bar-border"/);
32
+ if (closeAt > 0)
33
+ block = block.slice(0, closeAt);
34
+ // Manual name is the first font-weight:800 anchor in the block.
35
+ const titleMatch = block.match(/<a[^>]*font-weight:\s*800;?[^>]*>([\s\S]*?)<\/a>/);
36
+ if (!titleMatch)
37
+ continue;
38
+ const rawTitle = titleMatch[1].replace(/<[^>]+>/g, "");
39
+ const manual = normalizeManualName(rawTitle);
40
+ if (!manual.startsWith("DM8-"))
41
+ continue;
42
+ const chapters = [];
43
+ const chapterRe = /<a[^>]*href="([^"]+\.html)"[^>]*>([\s\S]*?)<\/a>/g;
44
+ let m;
45
+ while ((m = chapterRe.exec(block)) !== null) {
46
+ const href = m[1];
47
+ if (!href.endsWith(".html") || href.includes("/") || href.includes(":"))
48
+ continue;
49
+ const title = m[2].replace(/<[^>]+>/g, "").replace(ICONFONT_RANGE, "").trim();
50
+ if (!title)
51
+ continue;
52
+ chapters.push({ href, title });
53
+ }
54
+ if (chapters.length > 0)
55
+ out.push({ manual, chapters });
56
+ }
57
+ return out;
58
+ }
59
+ export function extractArticleHtml(html) {
60
+ // The eco.dameng.com pages wrap the actual chapter content in
61
+ // <div class="article-content vditor-reset" ...> ... </div>
62
+ // followed by a <footer class="article-footer"> we want to drop.
63
+ const m = html.match(/<div\s+class="article-content[^"]*"[^>]*>([\s\S]*?)<(?:footer|div|aside)[^>]*\sclass="article-footer/);
64
+ return m ? m[1].trim() : "";
65
+ }
66
+ let _turndown = null;
67
+ function turndown() {
68
+ if (_turndown)
69
+ return _turndown;
70
+ const t = new TurndownService({
71
+ headingStyle: "atx",
72
+ codeBlockStyle: "fenced",
73
+ bulletListMarker: "-",
74
+ emDelimiter: "*",
75
+ });
76
+ // Drop the inline anchor links Hexo adds after every heading.
77
+ t.addRule("strip-article-anchor", {
78
+ filter: (node) => node.nodeName === "A" &&
79
+ (node.getAttribute("class") || "").includes("article-anchor"),
80
+ replacement: () => "",
81
+ });
82
+ // Drop "copy code" buttons commonly added by docs sites.
83
+ t.addRule("strip-copy-button", {
84
+ filter: (node) => node.nodeName === "BUTTON" ||
85
+ (node.getAttribute("class") || "").includes("copy"),
86
+ replacement: () => "",
87
+ });
88
+ _turndown = t;
89
+ return t;
90
+ }
91
+ export function htmlToMarkdown(html) {
92
+ return turndown().turndown(html).trim() + "\n";
93
+ }
94
+ export async function fetchAndConvertAll(opts) {
95
+ const concurrency = opts.concurrency ?? 5;
96
+ const log = opts.onProgress ?? (() => { });
97
+ const baseUrl = opts.source.endsWith("/") ? opts.source : opts.source + "/";
98
+ log(`fetching index: ${baseUrl}`);
99
+ const indexHtml = await fetchText(baseUrl);
100
+ const manuals = parseManualsFromSidebar(indexHtml);
101
+ if (manuals.length === 0) {
102
+ throw new Error(`No manuals found in sidebar at ${baseUrl}. Did the site structure change?`);
103
+ }
104
+ log(`found ${manuals.length} manuals, ${manuals.reduce((n, m) => n + m.chapters.length, 0)} chapters total`);
105
+ await mkdir(opts.targetDir, { recursive: true });
106
+ await writeFile(join(opts.targetDir, "README.md"), `# Dameng manuals (auto-fetched)\n\nGenerated from ${baseUrl} at ${new Date().toISOString()}.\n\nCopyright belongs to 达梦数据库股份有限公司. Kept locally for personal/team reference.\n`, "utf8");
107
+ const tasks = [];
108
+ for (const group of manuals) {
109
+ const dir = join(opts.targetDir, group.manual);
110
+ await mkdir(dir, { recursive: true });
111
+ for (const ch of group.chapters) {
112
+ tasks.push({
113
+ manual: group.manual,
114
+ chapter: ch,
115
+ url: baseUrl + ch.href,
116
+ outPath: join(dir, ch.href.replace(/\.html$/, ".md")),
117
+ });
118
+ }
119
+ }
120
+ const failures = [];
121
+ let completed = 0;
122
+ const total = tasks.length;
123
+ async function worker(slice) {
124
+ for (const t of slice) {
125
+ try {
126
+ const html = await fetchText(t.url);
127
+ const article = extractArticleHtml(html);
128
+ if (!article)
129
+ throw new Error("no article body in HTML");
130
+ const md = `# ${t.chapter.title}\n\n` + htmlToMarkdown(article);
131
+ await writeFile(t.outPath, md, "utf8");
132
+ }
133
+ catch (e) {
134
+ failures.push({ url: t.url, error: e instanceof Error ? e.message : String(e) });
135
+ }
136
+ completed++;
137
+ if (completed % 10 === 0 || completed === total) {
138
+ log(` [${completed}/${total}] ${t.manual} :: ${t.chapter.title}`);
139
+ }
140
+ }
141
+ }
142
+ // Slice tasks evenly across workers.
143
+ const slices = Array.from({ length: concurrency }, () => []);
144
+ tasks.forEach((t, i) => slices[i % concurrency].push(t));
145
+ await Promise.all(slices.map((s) => worker(s)));
146
+ return {
147
+ manualCount: manuals.length,
148
+ chapterCount: tasks.length,
149
+ failures,
150
+ };
151
+ }
152
+ async function fetchText(url, retries = 2) {
153
+ let lastErr;
154
+ for (let attempt = 0; attempt <= retries; attempt++) {
155
+ try {
156
+ const res = await fetch(url, {
157
+ headers: {
158
+ "User-Agent": "basedoc-dameng-mcp/0.1.2 (+https://github.com/bravejack/basedoc-dameng-mcp)",
159
+ "Accept": "text/html",
160
+ },
161
+ });
162
+ if (!res.ok)
163
+ throw new Error(`HTTP ${res.status} for ${url}`);
164
+ return await res.text();
165
+ }
166
+ catch (e) {
167
+ lastErr = e;
168
+ if (attempt < retries) {
169
+ await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
170
+ }
171
+ }
172
+ }
173
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
174
+ }
175
+ //# sourceMappingURL=fetch-docs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-docs.js","sourceRoot":"","sources":["../src/fetch-docs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,eAAe,MAAM,UAAU,CAAC;AAYvC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAEhC,MAAM,oBAAoB,GAAG,yBAAyB,CAAC;AAEvD,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,OAAO,GAAG;SACP,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC;SACjC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC;SAC3B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE;SACN,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,IAAY;IAClD,6EAA6E;IAC7E,4EAA4E;IAC5E,yEAAyE;IACzE,uCAAuC;IACvC,MAAM,OAAO,GAAG,mDAAmD,CAAC;IACpE,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,IAAI,EAA0B,CAAC;IAC/B,OAAO,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI;QAAE,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IAEpE,MAAM,GAAG,GAAkB,EAAE,CAAC;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;QACvE,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACnC,oEAAoE;QACpE,kDAAkD;QAClD,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,2BAA2B,CAAC,CAAC;QAC1D,IAAI,OAAO,GAAG,CAAC;YAAE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QAEjD,gEAAgE;QAChE,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAC5B,kDAAkD,CACnD,CAAC;QACF,IAAI,CAAC,UAAU;YAAE,SAAS;QAC1B,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAE,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,SAAS;QAEzC,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,SAAS,GAAG,mDAAmD,CAAC;QACtE,IAAI,CAAyB,CAAC;QAC9B,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;YACnB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAAE,SAAS;YAClF,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC/E,IAAI,CAAC,KAAK;gBAAE,SAAS;YACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACjC,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,8DAA8D;IAC9D,8DAA8D;IAC9D,iEAAiE;IACjE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAClB,sGAAsG,CACvG,CAAC;IACF,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC/B,CAAC;AAED,IAAI,SAAS,GAA2B,IAAI,CAAC;AAC7C,SAAS,QAAQ;IACf,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAChC,MAAM,CAAC,GAAG,IAAI,eAAe,CAAC;QAC5B,YAAY,EAAE,KAAK;QACnB,cAAc,EAAE,QAAQ;QACxB,gBAAgB,EAAE,GAAG;QACrB,WAAW,EAAE,GAAG;KACjB,CAAC,CAAC;IACH,8DAA8D;IAC9D,CAAC,CAAC,OAAO,CAAC,sBAAsB,EAAE;QAChC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CACf,IAAI,CAAC,QAAQ,KAAK,GAAG;YACrB,CAAE,IAAoB,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,gBAAgB,CAAC;QAChF,WAAW,EAAE,GAAG,EAAE,CAAC,EAAE;KACtB,CAAC,CAAC;IACH,yDAAyD;IACzD,CAAC,CAAC,OAAO,CAAC,mBAAmB,EAAE;QAC7B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CACf,IAAI,CAAC,QAAQ,KAAK,QAAQ;YAC1B,CAAE,IAAoB,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;QACtE,WAAW,EAAE,GAAG,EAAE,CAAC,EAAE;KACtB,CAAC,CAAC;IACH,SAAS,GAAG,CAAC,CAAC;IACd,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,QAAQ,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC;AACjD,CAAC;AAeD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAAe;IACtD,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC;IAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC;IAE5E,GAAG,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAC;IAClC,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,uBAAuB,CAAC,SAAS,CAAC,CAAC;IACnD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,kCAAkC,OAAO,kCAAkC,CAAC,CAAC;IAC/F,CAAC;IACD,GAAG,CAAC,SAAS,OAAO,CAAC,MAAM,aAAa,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,iBAAiB,CAAC,CAAC;IAE7G,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,MAAM,SAAS,CACb,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,EACjC,qDAAqD,OAAO,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,oFAAoF,EAC/K,MAAM,CACP,CAAC;IAEF,MAAM,KAAK,GAAyE,EAAE,CAAC;IACvF,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAC/C,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACtC,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAChC,KAAK,CAAC,IAAI,CAAC;gBACT,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,OAAO,EAAE,EAAE;gBACX,GAAG,EAAE,OAAO,GAAG,EAAE,CAAC,IAAI;gBACtB,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;aACtD,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAA4B,EAAE,CAAC;IAC7C,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC;IAE3B,KAAK,UAAU,MAAM,CAAC,KAAmB;QACvC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;gBACpC,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;gBACzC,IAAI,CAAC,OAAO;oBAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;gBACzD,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,MAAM,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;gBAChE,MAAM,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;YACzC,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACnF,CAAC;YACD,SAAS,EAAE,CAAC;YACZ,IAAI,SAAS,GAAG,EAAE,KAAK,CAAC,IAAI,SAAS,KAAK,KAAK,EAAE,CAAC;gBAChD,GAAG,CAAC,MAAM,SAAS,IAAI,KAAK,KAAK,CAAC,CAAC,MAAM,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;YACrE,CAAC;QACH,CAAC;IACH,CAAC;IAED,qCAAqC;IACrC,MAAM,MAAM,GAAmB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7E,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,GAAG,WAAW,CAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEhD,OAAO;QACL,WAAW,EAAE,OAAO,CAAC,MAAM;QAC3B,YAAY,EAAE,KAAK,CAAC,MAAM;QAC1B,QAAQ;KACT,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,OAAO,GAAG,CAAC;IAC/C,IAAI,OAAgB,CAAC;IACrB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;QACpD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,OAAO,EAAE;oBACP,YAAY,EAAE,6EAA6E;oBAC3F,QAAQ,EAAE,WAAW;iBACtB;aACF,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,QAAQ,GAAG,EAAE,CAAC,CAAC;YAC9D,OAAO,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC1B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,GAAG,CAAC,CAAC;YACZ,IAAI,OAAO,GAAG,OAAO,EAAE,CAAC;gBACtB,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAC/D,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;AACxE,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "basedoc-dameng-mcp",
3
- "version": "0.1.1",
4
- "description": "Read-only MCP server for Dameng (DM8): SQL whitelist + local Markdown manual search (list_manuals / list_sections / read_section / lookup_docs).",
3
+ "version": "0.1.2",
4
+ "description": "面向达梦数据库 (DM8) 的只读 MCP 服务:受限 SQL 查询 + 本地 Markdown 手册离线全文检索(list_manuals / list_sections / read_section / lookup_docs)。",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "bravejack",
@@ -24,7 +24,8 @@
24
24
  "documentation"
25
25
  ],
26
26
  "bin": {
27
- "basedoc-dameng-mcp": "dist/server.js"
27
+ "basedoc-dameng-mcp": "dist/server.js",
28
+ "basedoc-dameng-fetch-docs": "dist/bin-fetch.js"
28
29
  },
29
30
  "main": "dist/server.js",
30
31
  "files": [
@@ -33,7 +34,7 @@
33
34
  "LICENSE"
34
35
  ],
35
36
  "scripts": {
36
- "build": "tsc -p tsconfig.json && chmod +x dist/server.js",
37
+ "build": "tsc -p tsconfig.json && chmod +x dist/server.js dist/bin-fetch.js",
37
38
  "test": "vitest run",
38
39
  "test:watch": "vitest",
39
40
  "start": "node dist/server.js",
@@ -42,10 +43,12 @@
42
43
  "dependencies": {
43
44
  "@modelcontextprotocol/sdk": "1.29.0",
44
45
  "dmdb": "1.0.48286",
46
+ "turndown": "7.2.4",
45
47
  "zod": "4.4.3"
46
48
  },
47
49
  "devDependencies": {
48
50
  "@types/node": "22.10.2",
51
+ "@types/turndown": "5.0.6",
49
52
  "typescript": "5.7.2",
50
53
  "vitest": "2.1.8"
51
54
  },