@hedeqiang/dsh-nl2sql 0.12.2 → 0.13.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/CHANGELOG.md +18 -1
- package/README.md +22 -0
- package/README.zh-CN.md +22 -0
- package/bin/nl2sql-mcp.mjs +103 -0
- package/cordis.patch.yml +4 -0
- package/docs/mcp.md +119 -0
- package/index.js +17 -1
- package/lib/client.js +1680 -6
- package/package.json +6 -1
- package/service/alerts.js +20 -6
- package/service/features.js +21 -0
- package/service/forecast.js +252 -0
- package/service/freshness.js +747 -0
- package/service/mcp.js +677 -0
- package/service/pptx.js +362 -0
- package/service/present.js +516 -0
- package/service/proactive.js +531 -0
- package/service/query.js +3 -2
- package/service/reconcile.js +565 -0
- package/service/shared.js +27 -0
- package/service/subscriptions.js +563 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.13.0] - 2026-11-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Data health:** per-table freshness (latest timestamp, lag, thresholds), the `nl2sql_freshness` tool and stale-data warnings under results and in the schema view, notifications when a table stops updating and when it recovers; a daily data-quality patrol (null rates, duplicate keys, outliers, future timestamps, negative amounts) with a health score and a push when it drops.
|
|
12
|
+
- **Reconciliation:** the `nl2sql_reconcile` tool and saved daily jobs that compare two queries on two data sources by key (tolerances, normalisation), with match rate, rows missing on either side, value differences, run history and pushes with the full differences as Excel.
|
|
13
|
+
- **Chat subscriptions:** `nl2sql_subscribe`, `nl2sql_alert_create` and `nl2sql_subscriptions` create, list, pause and cancel scheduled results and alerts delivered to the chat they were created in, running with the creator's permissions at delivery time.
|
|
14
|
+
- **Data finds people:** daily digests of notable metric changes with their main contributors, automatic attribution in alert messages, metric goals with progress and projection on dashboards, and time-series forecasts (`nl2sql_forecast`, result menu).
|
|
15
|
+
- **Presentation:** PowerPoint and long-image export for dashboards and results, sending the long image to Lark / Telegram, presentation mode, and scheduled sync of results to Lark Bitable.
|
|
16
|
+
- **MCP server:** a local Model Context Protocol endpoint with per-client tokens mapped to owner, member or role scopes, plus the `nl2sql-mcp` stdio bridge for Claude Desktop ([docs/mcp.md](docs/mcp.md)).
|
|
17
|
+
- The **Manage** navigation groups sections into core, *Insight & automation* and *Integrations*.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Internal: feature modules plug in through `service/features.js` (RPCs, tools, scheduler jobs) and client extension points (`extend.*`); `runQuery` accepts a per-call `maxRows` up to 200,000.
|
|
21
|
+
|
|
22
|
+
These features are tested with local databases and fake IM / Lark servers; real Lark, Telegram, WeCom, DingTalk and MCP clients have not been exercised yet.
|
|
23
|
+
|
|
8
24
|
## [0.12.2] - 2026-11-12
|
|
9
25
|
|
|
10
26
|
### Security
|
|
@@ -106,7 +122,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
106
122
|
|
|
107
123
|
- Redesigned UI; Lark push (Markdown and native charts); access control, feedback loop, dashboard global filters; PostgreSQL / Elasticsearch, SSH tunnels, privacy settings, automatic overviews, scheduled reports; semantic layer, ER graph, scan guard, history and audit, dashboards; first version.
|
|
108
124
|
|
|
109
|
-
[Unreleased]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.
|
|
125
|
+
[Unreleased]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.13.0...HEAD
|
|
126
|
+
[0.13.0]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.12.2...v0.13.0
|
|
110
127
|
[0.12.2]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.12.1...v0.12.2
|
|
111
128
|
[0.12.1]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.12.0...v0.12.1
|
|
112
129
|
[0.12.0]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.11.2...v0.12.0
|
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ English · [简体中文](README.zh-CN.md)
|
|
|
24
24
|
- [Installation](#installation)
|
|
25
25
|
- [Quick start](#quick-start)
|
|
26
26
|
- [IM channels and bots](#im-channels-and-bots)
|
|
27
|
+
- [MCP server](#mcp-server)
|
|
27
28
|
- [Security model](#security-model)
|
|
28
29
|
- [Data and privacy](#data-and-privacy)
|
|
29
30
|
- [Limitations](#limitations)
|
|
@@ -47,6 +48,17 @@ English · [简体中文](README.zh-CN.md)
|
|
|
47
48
|
- Push to Lark / Feishu, Telegram, WeCom and DingTalk; charts are sent as native card charts, images or tables depending on the platform, and large results come with the full data as an Excel attachment where the platform allows files.
|
|
48
49
|
- Chat bots that answer @-mentions in group chats without a public URL (Lark long connection, Telegram long polling, WeCom AI bot long connection, DingTalk Stream mode).
|
|
49
50
|
|
|
51
|
+
**Data finds people**
|
|
52
|
+
- Data freshness: the latest timestamp of every table, stale-data warnings in answers and alerts when a table stops updating; a daily data-quality patrol with a health score.
|
|
53
|
+
- Cross-source reconciliation: compare two queries on two data sources by key, list rows missing on either side and value differences; save as a daily job that pushes differences with an Excel attachment.
|
|
54
|
+
- Chat subscriptions: "@bot send this to the group every day at 9" or "tell me when withdrawals exceed 500k" creates a subscription or alert delivered to that chat.
|
|
55
|
+
- Daily digest with the notable changes and their main contributors, automatic attribution in alert messages, metric goals with progress and projection, and time-series forecasts.
|
|
56
|
+
|
|
57
|
+
**Present and connect**
|
|
58
|
+
- Export a dashboard or results to PowerPoint or one long image (also sendable to Lark / Telegram); presentation mode for wall screens with auto refresh and rotation.
|
|
59
|
+
- Sync query results to Lark Bitable on a schedule.
|
|
60
|
+
- MCP server: Claude Desktop, Cursor, VS Code and other MCP clients can use the same governed access (see [docs/mcp.md](docs/mcp.md)).
|
|
61
|
+
|
|
50
62
|
**Govern**
|
|
51
63
|
- Read-only by construction, table / column access policies with masking, and a full-scan guard.
|
|
52
64
|
- Members with roles (admin / analyst / viewer), data source scopes and row-level rules for people and group chats on every IM platform.
|
|
@@ -126,6 +138,16 @@ In a group chat, members @-mention the bot and ask. The bot knows which data sou
|
|
|
126
138
|
|
|
127
139
|
> The Telegram, WeCom and DingTalk adapters are tested against local fakes of their APIs and have not yet been verified with real accounts; Lark has been used with real workspaces. Reports from real deployments are welcome.
|
|
128
140
|
|
|
141
|
+
## MCP server
|
|
142
|
+
|
|
143
|
+
Other AI tools — Claude Desktop, Cursor, Cline, VS Code — can query your data sources through the [Model Context Protocol](https://modelcontextprotocol.io/), with the same read-only admission, access policies, member scopes, row-level rules, privacy setting and audit. Enable it under **Admin → MCP access** (off by default; listens on `127.0.0.1:47860` only), create a scoped token (shown once), and paste the generated snippet into your client. HTTP clients use `http://127.0.0.1:47860/mcp` with `Authorization: Bearer <token>`; stdio clients such as Claude Desktop use the bundled bridge:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
NL2SQL_MCP_TOKEN=<token> npx -y -p @hedeqiang/dsh-nl2sql nl2sql-mcp
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Tools: `list_datasources`, `get_schema`, `query`, `query_metric`, `search_knowledge`, `drill`, `explain_change` (plus `reconcile` / `forecast` when available); resources: the schema of every visible data source. See [docs/mcp.md](docs/mcp.md).
|
|
150
|
+
|
|
129
151
|
## Security model
|
|
130
152
|
|
|
131
153
|
- **Read-only queries.** Every statement passes a per-dialect lexer (MySQL, PostgreSQL, SQLite, Elasticsearch SQL) that admits only `SELECT` / `SHOW` / `DESCRIBE` / `EXPLAIN`; queries run in read-only transactions with server-side timeouts, PostgreSQL uses the extended protocol (no multi-statement strings), and large results are streamed and truncated.
|
package/README.zh-CN.md
CHANGED
|
@@ -24,6 +24,7 @@ Agent 编写只读 SQL、执行并在对话中画图。<br>
|
|
|
24
24
|
- [安装](#安装)
|
|
25
25
|
- [快速开始](#快速开始)
|
|
26
26
|
- [IM 渠道与机器人](#im-渠道与机器人)
|
|
27
|
+
- [MCP 服务](#mcp-服务)
|
|
27
28
|
- [安全模型](#安全模型)
|
|
28
29
|
- [数据与隐私](#数据与隐私)
|
|
29
30
|
- [已知限制](#已知限制)
|
|
@@ -47,6 +48,17 @@ Agent 编写只读 SQL、执行并在对话中画图。<br>
|
|
|
47
48
|
- 推送到飞书、Telegram、企业微信、钉钉:图表按平台能力用原生卡片图表、图片或表格发送;结果过大时,在能发文件的平台附带完整数据 Excel。
|
|
48
49
|
- 群聊机器人回答 @提问,不需要公网地址(飞书长连接、Telegram 长轮询、企业微信智能机器人长连接、钉钉 Stream 模式)。
|
|
49
50
|
|
|
51
|
+
**让数据找人**
|
|
52
|
+
- 数据新鲜度:记录每张表的最新数据时间,回答近期数据时提示是否滞后,数据断更时告警;每日数据质量巡检并给出健康分。
|
|
53
|
+
- 跨数据源对账:按主键对比两个数据源的两条查询,列出单边缺失和数值差异;可保存为每日任务,有差异时推送并附 Excel。
|
|
54
|
+
- 群里订阅:「@机器人 每天 9 点把这个发到群里」「提现超过 50 万提醒我」直接创建订阅或告警,推送到当前群。
|
|
55
|
+
- 每日数据简报(异常变化及主要来源)、告警消息自动归因、指标目标进度与达成预测、时间序列预测。
|
|
56
|
+
|
|
57
|
+
**展示与集成**
|
|
58
|
+
- 看板或结果导出 PPT、长图(可发送到飞书 / Telegram);大屏模式(全屏、自动刷新、轮播)。
|
|
59
|
+
- 定时把查询结果同步到飞书多维表格。
|
|
60
|
+
- MCP 服务:Claude Desktop、Cursor、VS Code 等 MCP 客户端可以使用同样受控的数据访问(见 [docs/mcp.md](docs/mcp.md))。
|
|
61
|
+
|
|
50
62
|
**治理**
|
|
51
63
|
- 只读执行;表 / 字段级访问控制与脱敏;大表全表扫描保护。
|
|
52
64
|
- 成员:各 IM 平台的个人和群都可以设置角色(管理员 / 分析师 / 只读)、数据源范围和行级权限。
|
|
@@ -126,6 +138,16 @@ dsh plugin --profile desktop remove @hedeqiang/dsh-nl2sql
|
|
|
126
138
|
|
|
127
139
|
> Telegram、企业微信、钉钉适配器目前只在本地模拟的平台接口上测试过,尚未在真实账号上验证;飞书已在真实环境中使用。欢迎反馈实际部署情况。
|
|
128
140
|
|
|
141
|
+
## MCP 服务
|
|
142
|
+
|
|
143
|
+
Claude Desktop、Cursor、Cline、VS Code 等 AI 工具可以通过 [Model Context Protocol](https://modelcontextprotocol.io/) 查询这里的数据源,只读准入、访问控制、成员范围、行级权限、隐私设置与审计同样生效。在 **管理 → MCP 接入** 中启用(默认关闭,只监听 `127.0.0.1:47860`),创建带权限范围的令牌(只显示一次),把页面生成的配置粘贴到客户端即可。HTTP 客户端使用 `http://127.0.0.1:47860/mcp` 并带上 `Authorization: Bearer <令牌>`;Claude Desktop 等 stdio 客户端使用内置的桥接命令:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
NL2SQL_MCP_TOKEN=<令牌> npx -y -p @hedeqiang/dsh-nl2sql nl2sql-mcp
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
工具:`list_datasources`、`get_schema`、`query`、`query_metric`、`search_knowledge`、`drill`、`explain_change`(安装了对账 / 预测功能时还有 `reconcile` / `forecast`);资源:每个可见数据源的表结构。详见 [docs/mcp.md](docs/mcp.md)(英文)。
|
|
150
|
+
|
|
129
151
|
## 安全模型
|
|
130
152
|
|
|
131
153
|
- **只读查询。** 每条语句都经过按方言的词法检查(MySQL、PostgreSQL、SQLite、Elasticsearch SQL),只允许 `SELECT` / `SHOW` / `DESCRIBE` / `EXPLAIN`;查询在只读事务中执行并有服务端超时;PostgreSQL 使用扩展协议(不接受多语句);大结果流式读取并截断。
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* nl2sql-mcp — stdio bridge to the DeepSeek Harness Data Q&A MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Speaks MCP over stdio (newline-delimited JSON-RPC 2.0) and forwards every
|
|
6
|
+
* message to the plugin's Streamable HTTP endpoint, so stdio-only clients
|
|
7
|
+
* (e.g. Claude Desktop) can use it. Plain ESM, Node ≥ 18, no dependencies.
|
|
8
|
+
*
|
|
9
|
+
* NL2SQL_MCP_URL default http://127.0.0.1:47860/mcp
|
|
10
|
+
* NL2SQL_MCP_TOKEN bearer token created in Data Q&A → MCP access (required)
|
|
11
|
+
*
|
|
12
|
+
* npx -y -p @hedeqiang/dsh-nl2sql nl2sql-mcp
|
|
13
|
+
*/
|
|
14
|
+
const URL_ = process.env.NL2SQL_MCP_URL || 'http://127.0.0.1:47860/mcp'
|
|
15
|
+
const TOKEN = process.env.NL2SQL_MCP_TOKEN || ''
|
|
16
|
+
const TIMEOUT_MS = Number(process.env.NL2SQL_MCP_TIMEOUT_MS) || 11 * 60 * 1000
|
|
17
|
+
|
|
18
|
+
const log = (...a) => process.stderr.write(`[nl2sql-mcp] ${a.join(' ')}\n`)
|
|
19
|
+
const write = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`)
|
|
20
|
+
const errorReply = (id, code, message, data) => write({ jsonrpc: '2.0', id: id ?? null, error: { code, message, ...(data !== undefined ? { data } : {}) } })
|
|
21
|
+
|
|
22
|
+
if (typeof fetch !== 'function') { log('Node.js 18 or newer is required (global fetch).'); process.exit(1) }
|
|
23
|
+
if (!TOKEN) log('NL2SQL_MCP_TOKEN is not set; requests will be rejected (401). Create a token in DeepSeek Harness → Data Q&A → MCP access.')
|
|
24
|
+
|
|
25
|
+
let sessionId
|
|
26
|
+
let protocolVersion
|
|
27
|
+
let pending = 0
|
|
28
|
+
let ended = false
|
|
29
|
+
|
|
30
|
+
/** Header-safe value (Base64 sentinel for non-ASCII), per the Streamable HTTP binding. */
|
|
31
|
+
function headerValue(v) {
|
|
32
|
+
const s = String(v)
|
|
33
|
+
return /^[\x20-\x7e]*$/.test(s) && s.trim() === s && !/^=\?base64\?.*\?=$/.test(s) ? s : `=?base64?${Buffer.from(s, 'utf8').toString('base64')}?=`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function forward(msg) {
|
|
37
|
+
const isRequest = msg && typeof msg === 'object' && 'id' in msg && typeof msg.method === 'string'
|
|
38
|
+
const headers = { 'content-type': 'application/json', accept: 'application/json, text/event-stream' }
|
|
39
|
+
if (TOKEN) headers.authorization = `Bearer ${TOKEN}`
|
|
40
|
+
if (sessionId) headers['mcp-session-id'] = sessionId
|
|
41
|
+
const metaVersion = msg?.params?._meta?.['io.modelcontextprotocol/protocolVersion']
|
|
42
|
+
const version = metaVersion || protocolVersion
|
|
43
|
+
if (version && msg?.method !== 'initialize') headers['mcp-protocol-version'] = version
|
|
44
|
+
if (typeof msg?.method === 'string') headers['mcp-method'] = msg.method
|
|
45
|
+
const name = msg?.params?.name ?? msg?.params?.uri
|
|
46
|
+
if (name !== undefined && metaVersion) headers['mcp-name'] = headerValue(name)
|
|
47
|
+
let res
|
|
48
|
+
try {
|
|
49
|
+
res = await fetch(URL_, { method: 'POST', headers, body: JSON.stringify(msg), signal: AbortSignal.timeout(TIMEOUT_MS) })
|
|
50
|
+
} catch (e) {
|
|
51
|
+
if (isRequest) errorReply(msg.id, -32000, `Cannot reach the nl2sql MCP server at ${URL_} (${e.cause?.code ?? e.message}). Enable it in DeepSeek Harness → Data Q&A → MCP access and keep DeepSeek Harness running.`)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
const sid = res.headers.get('mcp-session-id')
|
|
55
|
+
if (sid) sessionId = sid
|
|
56
|
+
const type = res.headers.get('content-type') ?? ''
|
|
57
|
+
const text = await res.text()
|
|
58
|
+
if (res.status === 202 || !text) {
|
|
59
|
+
if (isRequest) errorReply(msg.id, -32000, `HTTP ${res.status} without a response body`)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
const emit = (body) => {
|
|
63
|
+
if (body?.result?.protocolVersion && msg?.method === 'initialize') protocolVersion = body.result.protocolVersion
|
|
64
|
+
write(body)
|
|
65
|
+
}
|
|
66
|
+
if (type.includes('text/event-stream')) {
|
|
67
|
+
// Request-scoped SSE stream: forward every `data:` event.
|
|
68
|
+
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
69
|
+
const data = block.split(/\r?\n/).filter(l => l.startsWith('data:')).map(l => l.slice(5).replace(/^ /, '')).join('\n')
|
|
70
|
+
if (data) { try { emit(JSON.parse(data)) } catch {} }
|
|
71
|
+
}
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
let body
|
|
75
|
+
try { body = JSON.parse(text) } catch {
|
|
76
|
+
if (isRequest) errorReply(msg.id, -32000, `HTTP ${res.status}: ${text.slice(0, 200)}`)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(body)) { for (const b of body) emit(b); return }
|
|
80
|
+
if (body && typeof body === 'object' && body.jsonrpc === '2.0') {
|
|
81
|
+
// Errors without an id (401 / 403) belong to this request.
|
|
82
|
+
if (isRequest && (body.id === null || body.id === undefined) && body.error) body.id = msg.id
|
|
83
|
+
if (res.status === 404 && sessionId && msg?.method !== 'initialize') sessionId = undefined
|
|
84
|
+
emit(body)
|
|
85
|
+
} else if (isRequest) errorReply(msg.id, -32000, `HTTP ${res.status}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let buffer = ''
|
|
89
|
+
process.stdin.setEncoding('utf8')
|
|
90
|
+
process.stdin.on('data', (chunk) => {
|
|
91
|
+
buffer += chunk
|
|
92
|
+
let i
|
|
93
|
+
while ((i = buffer.indexOf('\n')) >= 0) {
|
|
94
|
+
const line = buffer.slice(0, i).trim()
|
|
95
|
+
buffer = buffer.slice(i + 1)
|
|
96
|
+
if (!line) continue
|
|
97
|
+
let msg
|
|
98
|
+
try { msg = JSON.parse(line) } catch { errorReply(null, -32700, 'Parse error'); continue }
|
|
99
|
+
pending++
|
|
100
|
+
forward(msg).catch(e => log('forward failed:', e.message)).finally(() => { pending--; if (ended && !pending) process.exit(0) })
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
process.stdin.on('end', () => { ended = true; if (!pending) process.exit(0) })
|
package/cordis.patch.yml
CHANGED
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
13. Local file data sources (type sqlite) hold user-uploaded CSV/Excel tables; write SQLite SQL for them (strftime/date, double-quoted identifiers, Chinese column names must be quoted).
|
|
40
40
|
14. Messages tagged [飞书…], [Telegram…], [企业微信…] or [钉钉…] come from IM chat members through a bot: answer in Markdown without asking questions (state assumptions instead); a member marked 只读成员 may only use nl2sql_metric or reuse verified example SQL from nl2sql_knowledge — if a query is refused for row-level or viewer restrictions, explain the restriction instead of retrying variations.
|
|
41
41
|
15. When the user confirms an answer is correct (e.g. "对", "没问题", "就是这个"), offer to store it; on agreement call nl2sql_knowledge action=save_example with the question and the final SQL.
|
|
42
|
+
16. In IM bot chats only: for "每天 9 点把这个发到群里" / "每周一推送" call nl2sql_subscribe (resultId of the result just shown, or a metric name + grain/range; time HH:MM, period daily/weekly/monthly); for "超过 X 提醒我" / "下跌 20% 告诉我" call nl2sql_alert_create (resultId or metric, condition threshold/change/anomaly, mentionMe when they say 提醒我); for "我的订阅" / "取消订阅" / "暂停告警" call nl2sql_subscriptions (list first, then pause/resume/cancel by id). Deliveries go to the current chat; confirm the schedule in one sentence. Page users are told to use 管理 → 告警与报告 instead.
|
|
43
|
+
17. For 对比 / 对账 / 核对 questions spanning two data sources (or two queries on one source) — e.g. "核对订单库和财务库昨天的金额", "两边的笔数对得上吗" — call nl2sql_reconcile once with one SQL per side (aggregate both to the same grain and return the key columns plus the values to compare, aliased consistently), keys and compare columns (add a small tolerance for amounts with rounding differences); do not run the two queries separately and compare by hand. Then explain the outcome in plain language: match rate, how many keys exist only on one side, which columns disagree and the size of the total differences, plus warnings (truncation, duplicate keys, masked keys); suggest likely causes (time-zone or date boundary, status filters, late-arriving data) and offer to save it as a scheduled reconciliation in 管理 → 对账.
|
|
44
|
+
18. For 预测 / forecast / "下周会是多少" questions, first get a daily (or weekly/monthly) time series with ONE measure (nl2sql_metric with grain, or nl2sql_query with time + value), then call nl2sql_forecast with its resultId and horizon; report the point forecast with its 95% range, name the method, and say it is a statistical extrapolation that ignores promotions, holidays and other known events.
|
|
45
|
+
19. For questions about recent periods (today / yesterday / this week / 最近 / 今天 / 昨天 / 本周), call nl2sql_freshness with the tables you query; when a table is stale or lagging (status stale/warn), say so before the numbers, e.g. "⚠️ withdrawals 最新数据停在昨天 17:55", and do not present missing recent data as a real drop.
|
|
42
46
|
|
|
43
47
|
You may only read data. Never attempt to modify data or schema; the tools reject anything but SELECT/SHOW/DESC/EXPLAIN.
|
|
44
48
|
- id: time-context
|
package/docs/mcp.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# MCP server
|
|
2
|
+
|
|
3
|
+
dsh-nl2sql can expose its governed data access to other AI tools — Claude Desktop, Cursor, Cline, VS Code (GitHub Copilot agent mode) and any other [Model Context Protocol](https://modelcontextprotocol.io/specification/) client. External models get the same guard rails as the built-in Data Q&A agent:
|
|
4
|
+
|
|
5
|
+
- every statement passes **read-only admission** (one SELECT / WITH / SHOW / EXPLAIN; writes, DDL, multiple statements and system catalogs are refused);
|
|
6
|
+
- **access policies** (hidden tables and columns, masked columns) and **sensitive-column masking** apply;
|
|
7
|
+
- each token runs as an **actor** — the local owner, an existing **member** (role, data source scope, row-level rules) or a **role + data sources** (+ optional row rule);
|
|
8
|
+
- the **privacy setting** "result data sent to the model" decides whether the model sees rows, per-column statistics, or only row counts;
|
|
9
|
+
- every call is **audited** in History with source `mcp` and the token name.
|
|
10
|
+
|
|
11
|
+
The server runs inside DeepSeek Harness, so Harness must be running while you use it.
|
|
12
|
+
|
|
13
|
+
## Enable it
|
|
14
|
+
|
|
15
|
+
1. Open **Data Q&A → 管理 / Admin → MCP 接入 / MCP access**.
|
|
16
|
+
2. Turn the server on. It listens on `http://127.0.0.1:47860/mcp` by default (change the port there; the server restarts automatically). It is **off by default** and never binds to other interfaces.
|
|
17
|
+
3. Click **New token**, give it a name and choose its scope:
|
|
18
|
+
- **Local owner** — the same access as asking on the page (policies still apply);
|
|
19
|
+
- **A member** — follows that member's role, data sources and row-level rules, live; disabling or deleting the member makes the token stop working;
|
|
20
|
+
- **Role + data sources** — analyst / viewer / admin on the selected data sources, with an optional row rule (`column IN (values)`).
|
|
21
|
+
4. Copy the token — it is shown **once**. Only its SHA-256 hash is stored; if you lose it, revoke it and create a new one.
|
|
22
|
+
|
|
23
|
+
The page shows ready-to-copy snippets for the clients below, filled in with the new token.
|
|
24
|
+
|
|
25
|
+
## Claude Desktop (stdio bridge)
|
|
26
|
+
|
|
27
|
+
Claude Desktop launches local MCP servers as processes speaking stdio. The package ships a small bridge, `nl2sql-mcp` (plain Node ≥ 18, no dependencies), that relays stdio to the HTTP endpoint. Add to `claude_desktop_config.json` (Settings → Developer → Edit Config):
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"nl2sql": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "-p", "@hedeqiang/dsh-nl2sql", "nl2sql-mcp"],
|
|
35
|
+
"env": {
|
|
36
|
+
"NL2SQL_MCP_URL": "http://127.0.0.1:47860/mcp",
|
|
37
|
+
"NL2SQL_MCP_TOKEN": "<YOUR_TOKEN>"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You can run the bridge by hand to check it: `NL2SQL_MCP_TOKEN=… npx -y -p @hedeqiang/dsh-nl2sql nl2sql-mcp`, then type `{"jsonrpc":"2.0","id":1,"method":"tools/list"}`.
|
|
45
|
+
|
|
46
|
+
| Variable | Default | Meaning |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `NL2SQL_MCP_URL` | `http://127.0.0.1:47860/mcp` | The MCP endpoint shown in the page |
|
|
49
|
+
| `NL2SQL_MCP_TOKEN` | — | Bearer token (required) |
|
|
50
|
+
| `NL2SQL_MCP_TIMEOUT_MS` | 660000 | Per-request timeout |
|
|
51
|
+
|
|
52
|
+
## Cursor
|
|
53
|
+
|
|
54
|
+
Cursor speaks Streamable HTTP directly. Add to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project):
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"nl2sql": {
|
|
60
|
+
"url": "http://127.0.0.1:47860/mcp",
|
|
61
|
+
"headers": { "Authorization": "Bearer <YOUR_TOKEN>" }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## VS Code
|
|
68
|
+
|
|
69
|
+
Add to `.vscode/mcp.json` (or the user-level MCP configuration):
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"servers": {
|
|
74
|
+
"nl2sql": {
|
|
75
|
+
"type": "http",
|
|
76
|
+
"url": "http://127.0.0.1:47860/mcp",
|
|
77
|
+
"headers": { "Authorization": "Bearer <YOUR_TOKEN>" }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Cline and other clients: use the HTTP URL with an `Authorization: Bearer …` header when the client supports remote/HTTP servers, otherwise the stdio bridge command above.
|
|
84
|
+
|
|
85
|
+
## What the server offers
|
|
86
|
+
|
|
87
|
+
**Tools** (all read-only):
|
|
88
|
+
|
|
89
|
+
| Tool | What it does |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `list_datasources` | Data sources this token may use, with engine and SQL dialect |
|
|
92
|
+
| `get_schema` | Schema overview (visible tables, column roles, relations, dialect notes, glossary, metrics) or full detail of `tables` |
|
|
93
|
+
| `query` | One read-only SQL statement → columns, rows / statistics / count per privacy setting, `rowCount`, `truncated`, `resultId` |
|
|
94
|
+
| `query_metric` | A governed metric from the metric library (grain, dimensions, range, filters) |
|
|
95
|
+
| `search_knowledge` | Verified question→SQL examples, glossary and metric definitions related to a question |
|
|
96
|
+
| `drill` | Breakdown / detail of an earlier `resultId` (row rules re-applied) |
|
|
97
|
+
| `explain_change` | Attribution: which dimensions explain the change between two periods of a result |
|
|
98
|
+
| `reconcile` | Compare two queries (e.g. the same figures from two databases) — listed when the reconcile feature is installed |
|
|
99
|
+
| `forecast` | Forecast a time-series result — listed when the forecast feature is installed |
|
|
100
|
+
|
|
101
|
+
Tool errors (refused SQL, row-level restrictions, unknown tables) come back as results with `isError: true` so the model can correct itself.
|
|
102
|
+
|
|
103
|
+
**Resources:** `nl2sql://datasource/{id}/schema` for every visible data source (and `nl2sql://datasource/{id}/table/{table}` for one table), as plain text.
|
|
104
|
+
|
|
105
|
+
**Prompts:** `ask_data` — a template that walks the model through list → schema → knowledge → query.
|
|
106
|
+
|
|
107
|
+
## Protocol details
|
|
108
|
+
|
|
109
|
+
- Streamable HTTP, `POST /mcp`, plain JSON responses (no SSE); `GET` / `DELETE` → `405`.
|
|
110
|
+
- Dual-era: the 2026-07-28 revision (stateless, per-request `_meta`, `server/discover`, header checks `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name`) and the `initialize`-based revisions 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05 (`initialize` negotiates the version and mints an `Mcp-Session-Id`; unknown sessions → `404`). `ping`, `notifications/*` (→ `202`) and JSON-RPC batches are accepted.
|
|
111
|
+
- Unsupported versions get error `-32022` with the supported list.
|
|
112
|
+
|
|
113
|
+
## Security notes
|
|
114
|
+
|
|
115
|
+
- **Local only.** The server binds to `127.0.0.1`. Requests whose `Host` is not `localhost` / `127.0.0.1` / `[::1]` are refused with `403` (DNS-rebinding defence), as are requests with a non-local `Origin` header — so web pages in your browser cannot call it.
|
|
116
|
+
- **Tokens.** Every request needs `Authorization: Bearer <token>`; missing or unknown tokens get `401`. Tokens are 192-bit random values; only SHA-256 hashes are stored in `~/.dsh/nl2sql/state.json`, compared in constant time. Revoke a token in the page to cut a client off immediately. "Last used" is shown per token.
|
|
117
|
+
- **Least privilege.** Prefer member- or role-scoped tokens for tools you do not fully trust. Owner tokens can read everything the page can (still read-only and policy-filtered).
|
|
118
|
+
- **Data leaving the machine.** Whatever the MCP client receives is sent to *its* model provider. Use the privacy setting (Admin → Settings → Privacy, or per data source) to send only statistics (`summary`) or nothing (`none`) instead of rows.
|
|
119
|
+
- **Audit.** History lists every MCP query with source `mcp`, the token name and the actor; refusals are logged as `blocked`.
|
package/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { methods as m8 } from './service/alerts.js'
|
|
|
25
25
|
import { methods as m9 } from './service/uploads.js'
|
|
26
26
|
import { methods as m10 } from './service/admin.js'
|
|
27
27
|
import { methods as mSuggest } from './service/suggestions.js'
|
|
28
|
+
import { FEATURES } from './service/features.js'
|
|
28
29
|
const { CHART_TYPES, DEFAULT_SETTINGS, RECORD_KINDS, RPC_PATH, Records, UserError, newId, pick, toUserError } = shared
|
|
29
30
|
|
|
30
31
|
export const name = 'nl2sql'
|
|
@@ -124,6 +125,16 @@ class Nl2SqlService {
|
|
|
124
125
|
return this.writing
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Scheduler jobs contributed by feature modules: every prototype method named
|
|
130
|
+
* `jobs_<feature>(now)` returns an array of { id, due(now), run() }.
|
|
131
|
+
*/
|
|
132
|
+
schedulerJobs(now) {
|
|
133
|
+
const out = []
|
|
134
|
+
for (const k of Object.getOwnPropertyNames(Nl2SqlService.prototype)) if (k.startsWith('jobs_')) { try { out.push(...(this[k](now) ?? [])) } catch (error) { console.warn('nl2sql: %s failed: %s', k, error.message) } }
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
|
|
127
138
|
async dispose() {
|
|
128
139
|
this.disposed = true
|
|
129
140
|
if (this.alertTimer) clearInterval(this.alertTimer)
|
|
@@ -145,7 +156,7 @@ class Nl2SqlService {
|
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
}
|
|
148
|
-
for (const m of [m0, m1, m2, m3, m4, m5, mChannels, m6, m7, m8, m9, m10, mSuggest]) Object.defineProperties(Nl2SqlService.prototype, Object.getOwnPropertyDescriptors(m))
|
|
159
|
+
for (const m of [m0, m1, m2, m3, m4, m5, mChannels, m6, m7, m8, m9, m10, mSuggest, ...FEATURES.map(f => f.methods)]) Object.defineProperties(Nl2SqlService.prototype, Object.getOwnPropertyDescriptors(m))
|
|
149
160
|
const DATASOURCE_PARAM = { type: 'string', description: 'Data source id or name. Omit to use the one bound to this session.' }
|
|
150
161
|
|
|
151
162
|
function textBlock(text) {
|
|
@@ -428,6 +439,8 @@ function displayMeta(result) {
|
|
|
428
439
|
|
|
429
440
|
function registerRpc(ctx, service) {
|
|
430
441
|
const methods = {
|
|
442
|
+
// Feature modules (service/features.js) contribute RPCs as rpc(service) → { name: fn }.
|
|
443
|
+
...Object.assign({}, ...FEATURES.map(f => f.rpc?.(service) ?? {})),
|
|
431
444
|
listDatasources: () => service.listDatasources(),
|
|
432
445
|
saveDatasource: (p) => service.saveDatasource(p),
|
|
433
446
|
deleteDatasource: (p) => service.deleteDatasource(p.id),
|
|
@@ -591,6 +604,9 @@ export async function apply(ctx, config) {
|
|
|
591
604
|
service.startBots().catch(error => console.warn('nl2sql bot: start failed: %s', error.message))
|
|
592
605
|
service.scheduleAlerts()
|
|
593
606
|
registerAnalysisTools(ctx, service)
|
|
607
|
+
// Feature modules register their own agent tools: tools(ctx, service, helpers).
|
|
608
|
+
for (const f of FEATURES) f.tools?.(ctx, service, { displayMeta, textBlock, DATASOURCE_PARAM })
|
|
609
|
+
for (const f of FEATURES) f.start?.(ctx, service)
|
|
594
610
|
}
|
|
595
611
|
|
|
596
612
|
function registerAnalysisTools(ctx, service) {
|