@wsh19991219/mcp-server 0.1.1 → 0.1.3

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,18 +1,28 @@
1
1
  # @data_mgr/mcp-server
2
2
 
3
- Data Manager MCP Server — 通过 MCP 协议对接 Data Manager 后端 API,提供数据分析查询能力。
3
+ Data Manager MCP Server — 通过 [MCP](https://modelcontextprotocol.io) 协议对接 Data Manager 后端 API,为 Claude Code 等 AI Agent 提供数据查询与分析能力。
4
4
 
5
- ## Install
5
+ ## 功能
6
+
7
+ - **鉴权管理**:浏览器一键登录,JWT 自动续期
8
+ - **表结构探索**:模糊搜表、查看列定义
9
+ - **数据查询**:预定义指标 + 自定义只读 SQL
10
+ - **多库支持**:主库及多个区域数据库 group
11
+ - **安全约束**:SQL 只读、敏感字段自动脱敏
12
+
13
+ ## 安装
6
14
 
7
15
  ```bash
8
16
  npm install -g @data_mgr/mcp-server
9
17
  ```
10
18
 
11
- Requires **Node.js 18+**.
19
+ 需要 **Node.js 18+**。
20
+
21
+ ## 快速集成
12
22
 
13
- ## Claude Code 集成
23
+ ### Claude Code
14
24
 
15
- `.claude/settings.json` 中添加:
25
+ 在项目根目录 `.claude/settings.json` 中添加:
16
26
 
17
27
  ```json
18
28
  {
@@ -24,22 +34,65 @@ Requires **Node.js 18+**.
24
34
  }
25
35
  ```
26
36
 
37
+ 或使用 npx(无需全局安装):
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "data-mgr": {
43
+ "command": "npx",
44
+ "args": ["-y", "@data_mgr/mcp-server"]
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ ### Cursor
51
+
52
+ 在 `.cursor/mcp.json` 中添加同样的配置。
53
+
27
54
  ## MCP 工具
28
55
 
29
- | 工具 | 功能 |
30
- |------|------|
31
- | `check_auth` | 检查登录状态 |
32
- | `login` | 浏览器登录 |
33
- | `analytics_query` | 预定义指标查询 |
34
- | `api_request` | 通用 API 请求(含自定义 SQL |
56
+ | 工具 | 功能 | 说明 |
57
+ | --- | --- | --- |
58
+ | `check_auth` | 检查登录状态 | 返回用户名、token 有效期 |
59
+ | `login` | 浏览器登录 | 打开浏览器完成 OAuth 登录 |
60
+ | `analytics_query` | 预定义指标查询 | 支持 metric、filters、limit 等参数 |
61
+ | `api_request` | 通用 REST 请求 | GET/POST/PUT/DELETE,含自定义 SQL |
62
+
63
+ ### api_request 端点
64
+
65
+ | 端点 | 方法 | 用途 |
66
+ | --- | --- | --- |
67
+ | `/health` | GET | 健康检查,返回可用数据库 group |
68
+ | `/api/v1/analytics/catalog` | GET | 列出可用指标和数据库 group |
69
+ | `/api/v1/analytics/schema?q=<关键词>` | GET | 模糊搜索表名 |
70
+ | `/api/v1/analytics/schema?tables=<表名>` | GET | 查看表的列结构 |
71
+ | `/api/v1/analytics/sql` | POST | 执行自定义只读 SQL |
35
72
 
36
73
  ## 环境变量
37
74
 
38
75
  | 变量 | 说明 | 默认值 |
39
- |------|------|--------|
40
- | `DATA_MGR_API_URL` | 后端 API 地址 | `http://127.0.0.1:8092` |
76
+ | --- | --- | --- |
77
+ | `DATA_MGR_API_URL` | 后端 API 地址 | `http://42.194.226.85:8092` |
41
78
  | `DATA_MGR_CONFIG_DIR` | 配置文件目录 | `~/.data-mgr` |
42
79
 
80
+ ## 使用示例
81
+
82
+ 安装后在 Claude Code 对话中直接使用自然语言:
83
+
84
+ - "查看 photonpay_card_transaction 的表结构"
85
+ - "查询 photonpay 渠道最近 24 小时的卡交易笔数和金额"
86
+ - "对比三个渠道昨日的交易量"
87
+ - "生成昨日渠道交易日报"
88
+
89
+ ## 相关包
90
+
91
+ | 包名 | 用途 |
92
+ | --- | --- |
93
+ | [`@data_mgr/cli`](https://www.npmjs.com/package/@data_mgr/cli) | Data Manager CLI,适用于 Cursor Agent |
94
+ | [`weather-query-cli`](https://www.npmjs.com/package/weather-query-cli) | 天气查询 CLI |
95
+
43
96
  ## License
44
97
 
45
98
  MIT
package/SKILL.md CHANGED
@@ -9,56 +9,123 @@ description: >-
9
9
 
10
10
  # Data Manager MCP Server
11
11
 
12
- 通过 MCP 协议对接 Data Manager 后端 API,提供数据分析查询能力。
12
+ 通过 MCP 协议对接 Data Manager 后端 API,为 AI Agent 提供数据查询与分析能力。
13
13
 
14
- ## MCP 工具总览
14
+ ## 快速开始
15
+
16
+ 1. 全局安装:`npm install -g @data_mgr/mcp-server`
17
+ 2. 注册 MCP 服务(见下方 [注册](#注册到-claude-code) 章节)
18
+ 3. 对话中触发登录:Agent 会自动调用 `login` 工具打开浏览器
19
+
20
+ ## MCP 工具
15
21
 
16
22
  | 工具 | 功能 | 关键参数 |
17
- |------|------|---------|
18
- | `check_auth` | 检查登录状态 | 无 |
19
- | `login` | 浏览器登录 | `openBrowser` (可选) |
20
- | `analytics_query` | 预定义指标查询 | `metric`(必填), `db`, `params`, `filters`, `limit` |
21
- | `api_request` | 通用 API 请求 | `method`(必填), `path`(必填), `body`, `query` |
23
+ | --- | --- | --- |
24
+ | `check_auth` | 检查登录状态,返回 username / token 有效期 | 无 |
25
+ | `login` | 浏览器登录获取 JWT | `openBrowser`: bool(默认 true) |
26
+ | `analytics_query` | 查询预定义指标 | `metric`(必填), `db`, `params`, `filters`, `limit` |
27
+ | `api_request` | 通用 REST 请求(GET/POST/PUT/DELETE) | `method`(必填), `path`(必填), `body`, `query` |
22
28
 
23
- ## 通过 api_request 可访问的 REST 端点
29
+ ### api_request 可访问的 REST 端点
24
30
 
25
- | 端点 | 方法 | 用途 | 对应 Skill |
26
- |------|------|------|-----------|
27
- | `/api/v1/analytics/catalog` | GET | 列出可用指标和数据库 group | data-query, data-analysis |
28
- | `/api/v1/analytics/schema?q=<关键词>` | GET | 模糊搜表名 | schema-explorer |
29
- | `/api/v1/analytics/schema?tables=<表名>` | GET | 查看表列结构 | schema-explorer |
30
- | `/api/v1/analytics/sql` | POST | 自定义只读 SQL | data-query, data-analysis |
31
- | `/health` | GET | 健康检查 | |
31
+ | 端点 | 方法 | 用途 |
32
+ | --- | --- | --- |
33
+ | `/health` | GET | 健康检查,返回可用数据库 group 列表 |
34
+ | `/api/v1/analytics/catalog` | GET | 列出可用指标和数据库 group |
35
+ | `/api/v1/analytics/schema?q=<关键词>` | GET | 模糊搜表名 |
36
+ | `/api/v1/analytics/schema?tables=<表名>` | GET | 查看指定表的列结构 |
37
+ | `/api/v1/analytics/sql` | POST | 执行自定义只读 SQL(SELECT/WITH) |
32
38
 
33
- ## 子 Skill 列表
39
+ ## 子 Skill
34
40
 
35
41
  | Skill | 触发词 | 用途 |
36
- |-------|--------|------|
37
- | `schema-explorer` | 查表/表结构/搜表/字段 | 表结构探索 |
38
- | `data-query` | 查数据/指标/统计/SQL | 数据查询(指标 + SQL |
39
- | `data-analysis` | 数据分析/对账/报表/漏斗 | 六步分析流程 |
40
- | `daily-channel-report` | 日报/每日报表/昨日交易 | 渠道交易日报 |
42
+ | --- | --- | --- |
43
+ | `schema-explorer` | 查表、表结构、搜表、字段 | 探索数据库表结构 |
44
+ | `data-query` | 查数据、指标、统计、SQL | 预定义指标查询 + 自定义 SQL |
45
+ | `data-analysis` | 数据分析、对账、报表、漏斗 | 六步数据分析流程 |
46
+ | `daily-channel-report` | 日报、每日报表、昨日交易 | 渠道交易日报(笔数、金额、趋势) |
47
+
48
+ ## 渠道速查
49
+
50
+ | channel_id | 渠道名 | 交易表名 | 主金额字段(USD) |
51
+ | --- | --- | --- | --- |
52
+ | 1 | Photon (photonpay) | `photonpay_card_transaction` | `txn_principal_change_amount` |
53
+ | 2 | Interlace | `interlace_card_transaction` | `transaction_amount` |
54
+ | 3 | PingPong | `pingpong_card_transaction` | `billing_amount` |
55
+
56
+ > **注意:** 各渠道金额字段不同,禁止混用。详见各子 Skill 文档。
57
+
58
+ ## 数据库 Group
59
+
60
+ | group 名 | 说明 |
61
+ | --- | --- |
62
+ | `default` | 主库(默认) |
63
+ | `prod_vn` | 越南 |
64
+ | `prod_vero` | Vero |
65
+ | `prod_zenia` | Zenia |
66
+ | `prod_jtpay` | JT Pay |
41
67
 
42
68
  ## 标准工作流
43
69
 
44
70
  ```
45
- 1. check_auth → 检查是否已登录
46
- 2. login → 未登录时触发浏览器登录
47
- 3. catalog/schema → 定位数据(api_request GET)
48
- 4. query/sql → 取数(analytics_query 或 api_request POST)
49
- 5. 格式化交付 → 结构化结论
71
+ 1. check_auth → 检查是否已登录
72
+ 2. login → 未登录时触发浏览器登录
73
+ 3. catalog / schema → 定位目标数据(api_request GET)
74
+ 4. query / sql → 取数(analytics_query 或 api_request POST SQL
75
+ 5. 格式化交付 → 结构化结论 + 洞察
76
+ ```
77
+
78
+ ## 示例
79
+
80
+ ### 查询表结构
81
+
82
+ ```json
83
+ // api_request
84
+ { "method": "GET", "path": "/api/v1/analytics/schema?tables=photonpay_card_transaction" }
85
+ ```
86
+
87
+ ### 执行自定义 SQL
88
+
89
+ ```json
90
+ // api_request
91
+ {
92
+ "method": "POST",
93
+ "path": "/api/v1/analytics/sql",
94
+ "body": {
95
+ "db": "default",
96
+ "sql": "SELECT COUNT(*) AS cnt, SUM(txn_principal_change_amount) AS total_usd FROM photonpay_card_transaction WHERE created_time >= DATE_SUB(NOW(), INTERVAL 24 HOUR)"
97
+ }
98
+ }
50
99
  ```
51
100
 
52
101
  ## 注册到 Claude Code
53
102
 
103
+ ### 方式一:全局安装后注册(推荐)
104
+
105
+ ```bash
106
+ npm install -g @data_mgr/mcp-server
107
+ ```
108
+
54
109
  在 `.claude/settings.json` 中添加:
55
110
 
56
111
  ```json
57
112
  {
58
113
  "mcpServers": {
59
114
  "data-mgr": {
60
- "command": "node",
61
- "args": ["$(npm root -g)/@data_mgr/mcp-server/bin/data-mgr-mcp.js"]
115
+ "command": "data-mgr-mcp"
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### 方式二:npx 直接运行(无需全局安装)
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "data-mgr": {
127
+ "command": "npx",
128
+ "args": ["-y", "@data_mgr/mcp-server"]
62
129
  }
63
130
  }
64
131
  }
@@ -67,23 +134,23 @@ description: >-
67
134
  ## 环境变量
68
135
 
69
136
  | 变量 | 说明 | 默认值 |
70
- |------|------|--------|
137
+ | --- | --- | --- |
71
138
  | `DATA_MGR_API_URL` | 后端 API 地址 | `http://127.0.0.1:8092` |
72
139
  | `DATA_MGR_CONFIG_DIR` | 配置文件目录 | `~/.data-mgr` |
73
140
 
74
141
  ## 错误处理
75
142
 
76
- | 错误 | 处理 |
77
- |------|------|
143
+ | 错误 | 处理方式 |
144
+ | --- | --- |
78
145
  | `Not logged in` | 调用 `login` 工具 |
79
146
  | `Login timed out` | 重新调用 `login`,手动打开返回的 URL |
80
147
  | Token 过期 | 重新调用 `login` |
81
- | API 403/401 | 检查 token 或重新登录 |
82
- | API 参数错误 | 检查 metric/db/SQL 参数 |
148
+ | API 401 / 403 | 检查 token 或重新登录 |
149
+ | API 参数错误 | 检查 metric / db / SQL 参数是否正确 |
83
150
 
84
151
  ## 安全
85
152
 
86
- - **禁止**在回复中输出完整 token
87
- - 写/删操作前确认用户意图
88
- - SQL 仅允许 SELECT/WITH(只读),禁止 DML、注释、多语句
89
- - 敏感数据(手机/身份证/邮箱/银行卡)会被后端自动脱敏
153
+ - **禁止**在回复中输出完整 token 或密码
154
+ - 写 / 删除操作前必须确认用户意图
155
+ - SQL 仅允许 `SELECT` / `WITH`(只读),禁止 DML、注释、多语句
156
+ - 敏感数据(手机、身份证、邮箱、银行卡)由后端自动脱敏
package/lib/config.js CHANGED
@@ -8,9 +8,15 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
8
8
 
9
9
  /** @typedef {{ apiUrl?: string, token?: string, username?: string, userId?: number, tenantId?: number, expiresIn?: string, expiresAt?: string, loggedInAt?: string }} DataMgrConfig */
10
10
 
11
- export const DEFAULT_API_URL = "http://127.0.0.1:8092";
11
+ /** Production API (port 8092 serves plain HTTP; use HTTPS only via reverse proxy on 443). */
12
+ export const DEFAULT_API_URL = "http://42.194.226.85:8092";
12
13
  export const LOGIN_PATH = "/api/v1/auth/login";
13
14
 
15
+ /** @param {string} url */
16
+ export function normalizeApiUrl(url) {
17
+ return String(url).trim().replace(/\/$/, "");
18
+ }
19
+
14
20
  /** @param {string | number | undefined} expiresIn @param {Date} [from] */
15
21
  export function computeExpiresAt(expiresIn, from = new Date()) {
16
22
  if (expiresIn === undefined || expiresIn === null || expiresIn === "") return undefined;
@@ -68,9 +74,9 @@ export function getConfigPath() {
68
74
  /** @returns {string} */
69
75
  export function getApiUrl() {
70
76
  const fromEnv = process.env.DATA_MGR_API_URL?.trim();
71
- if (fromEnv) return fromEnv.replace(/\/$/, "");
77
+ if (fromEnv) return normalizeApiUrl(fromEnv);
72
78
  const cfg = loadConfig();
73
- if (cfg.apiUrl) return cfg.apiUrl.replace(/\/$/, "");
79
+ if (cfg.apiUrl) return normalizeApiUrl(cfg.apiUrl);
74
80
  return DEFAULT_API_URL;
75
81
  }
76
82
 
@@ -3,6 +3,40 @@ import { randomBytes } from "node:crypto";
3
3
  import { spawn } from "node:child_process";
4
4
  import { getApiUrl, LOGIN_PATH } from "./config.js";
5
5
 
6
+ const CORS_HEADERS = {
7
+ "Access-Control-Allow-Origin": "*",
8
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
9
+ "Access-Control-Allow-Headers": "Content-Type",
10
+ };
11
+
12
+ /** @param {import('node:http').ServerResponse} res @param {number} status @param {Record<string, unknown>} body */
13
+ function sendJson(res, status, body) {
14
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", ...CORS_HEADERS });
15
+ res.end(JSON.stringify(body));
16
+ }
17
+
18
+ /** @param {unknown} err @param {string} apiUrl */
19
+ function formatUpstreamError(err, apiUrl) {
20
+ const base = err instanceof Error ? err : new Error(String(err));
21
+ const cause = base.cause instanceof Error ? base.cause : null;
22
+ const code = /** @type {{ code?: string }} */ (cause ?? base).code;
23
+ const msg = cause?.message ?? base.message;
24
+
25
+ if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT") {
26
+ return `无法连接 API(${apiUrl}):${msg}`;
27
+ }
28
+ if (/packet length too long/i.test(msg)) {
29
+ return `协议不匹配:${apiUrl} 使用了 HTTPS,但该端口可能只提供 HTTP。请改为 http:// 或配置 443 反向代理。`;
30
+ }
31
+ if (/certificate|UNABLE_TO_VERIFY|self signed|ALTNAME_INVALID/i.test(msg)) {
32
+ return `HTTPS 证书校验失败(${apiUrl})。请使用与证书匹配的域名,或配置 NODE_EXTRA_CA_CERTS。`;
33
+ }
34
+ if (base.name === "TypeError" && /fetch failed/i.test(base.message)) {
35
+ return `请求 API 失败(${apiUrl}):${msg || base.message}`;
36
+ }
37
+ return msg || base.message;
38
+ }
39
+
6
40
  const LOGIN_PAGE = String.raw`<!DOCTYPE html>
7
41
  <html lang="zh-CN">
8
42
  <head>
@@ -130,14 +164,14 @@ const SUCCESS_PAGE = String.raw`<!DOCTYPE html>
130
164
  /**
131
165
  * Start local login server, open browser, return token when login succeeds.
132
166
  * Modified for MCP: uses onLoginUrl callback instead of console.log.
133
- * @param {{ timeoutMs?: number, openBrowser?: boolean, onLoginUrl?: (url: string) => void }} [options]
167
+ * @param {{ timeoutMs?: number, openBrowser?: boolean, apiUrl?: string, onLoginUrl?: (url: string) => void }} [options]
134
168
  * @returns {Promise<LoginResult>}
135
169
  */
136
170
  export function runBrowserLogin(options = {}) {
137
171
  const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
138
172
  const openBrowser = options.openBrowser !== false;
139
173
  const onLoginUrl = options.onLoginUrl ?? (() => {});
140
- const apiUrl = getApiUrl();
174
+ const apiUrl = options.apiUrl ?? getApiUrl();
141
175
  const state = randomBytes(16).toString("hex");
142
176
 
143
177
  return new Promise((resolve, reject) => {
@@ -154,6 +188,12 @@ export function runBrowserLogin(options = {}) {
154
188
  server = http.createServer(async (req, res) => {
155
189
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
156
190
 
191
+ if (req.method === "OPTIONS" && url.pathname === "/api/login") {
192
+ res.writeHead(204, CORS_HEADERS);
193
+ res.end();
194
+ return;
195
+ }
196
+
157
197
  if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/login")) {
158
198
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
159
199
  res.end(LOGIN_PAGE.replace("__API_URL__", apiUrl));
@@ -165,26 +205,34 @@ export function runBrowserLogin(options = {}) {
165
205
  req.on("data", (chunk) => { body += chunk; });
166
206
  req.on("end", async () => {
167
207
  try {
168
- const { username, password } = JSON.parse(body);
208
+ const { username, password } = JSON.parse(body || "{}");
169
209
  if (!username || !password) {
170
- res.writeHead(400, { "Content-Type": "application/json" });
171
- res.end(JSON.stringify({ error: "username and password required" }));
210
+ sendJson(res, 400, { error: "username and password required" });
172
211
  return;
173
212
  }
174
213
 
175
- const loginRes = await fetch(`${apiUrl}${LOGIN_PATH}`, {
176
- method: "POST",
177
- headers: { "Content-Type": "application/json" },
178
- body: JSON.stringify({ username, password }),
179
- });
214
+ const loginEndpoint = `${apiUrl}${LOGIN_PATH}`;
215
+ let loginRes;
216
+ try {
217
+ loginRes = await fetch(loginEndpoint, {
218
+ method: "POST",
219
+ headers: { "Content-Type": "application/json" },
220
+ body: JSON.stringify({ username, password }),
221
+ });
222
+ } catch (fetchErr) {
223
+ sendJson(res, 502, {
224
+ error: formatUpstreamError(fetchErr, apiUrl),
225
+ message: formatUpstreamError(fetchErr, apiUrl),
226
+ });
227
+ return;
228
+ }
180
229
 
181
230
  const data = await loginRes.json().catch(() => ({}));
182
231
 
183
232
  // API returns {code, message, data} — check code for errors
184
233
  if (!loginRes.ok || (data.code != null && data.code !== 0)) {
185
234
  const message = data.message ?? data.error ?? `Login failed (HTTP ${loginRes.status}, code ${data.code})`;
186
- res.writeHead(loginRes.ok ? 401 : loginRes.status, { "Content-Type": "application/json" });
187
- res.end(JSON.stringify({ error: message }));
235
+ sendJson(res, loginRes.ok ? 401 : loginRes.status, { error: message, message });
188
236
  return;
189
237
  }
190
238
 
@@ -192,16 +240,15 @@ export function runBrowserLogin(options = {}) {
192
240
  const inner = data.data ?? {};
193
241
  const token = data.token ?? inner.token ?? inner.access_token;
194
242
  if (!token) {
195
- res.writeHead(502, { "Content-Type": "application/json" });
196
- res.end(JSON.stringify({
243
+ sendJson(res, 502, {
197
244
  error: "API response missing token field",
245
+ message: "API response missing token field",
198
246
  debug: { topKeys: Object.keys(data), innerKeys: Object.keys(inner) },
199
- }));
247
+ });
200
248
  return;
201
249
  }
202
250
 
203
- res.writeHead(200, { "Content-Type": "application/json" });
204
- res.end(JSON.stringify({ ok: true }));
251
+ sendJson(res, 200, { ok: true });
205
252
 
206
253
  const loggedInAt = new Date().toISOString();
207
254
  const expiresIn = (inner.expires_in ?? data.expires_in) != null
@@ -218,9 +265,9 @@ export function runBrowserLogin(options = {}) {
218
265
  loginUrl: `http://127.0.0.1:${server?.address()?.port ?? "?"}/login`,
219
266
  });
220
267
  } catch (err) {
221
- const message = err instanceof Error ? err.message : String(err);
222
- res.writeHead(500, { "Content-Type": "application/json" });
223
- res.end(JSON.stringify({ error: message }));
268
+ const message = formatUpstreamError(err, apiUrl);
269
+ console.error("[data-mgr login]", message, err);
270
+ sendJson(res, 500, { error: message, message });
224
271
  }
225
272
  });
226
273
  return;
package/lib/tools.js CHANGED
@@ -184,6 +184,7 @@ export function registerTools(server) {
184
184
  const expiresAt = computeExpiresAt(result.expiresIn, new Date(result.loggedInAt));
185
185
 
186
186
  saveConfig({
187
+ apiUrl: getApiUrl(),
187
188
  token: result.token,
188
189
  username: result.username,
189
190
  userId: result.userId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsh19991219/mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "MCP server for Data Manager API — stdio transport, JWT auth",
5
5
  "type": "module",
6
6
  "main": "lib/tools.js",