@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 +66 -13
- package/SKILL.md +104 -37
- package/lib/config.js +9 -3
- package/lib/login-server.js +67 -20
- package/lib/tools.js +1 -0
- package/package.json +1 -1
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
|
-
##
|
|
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
|
-
|
|
19
|
+
需要 **Node.js 18+**。
|
|
20
|
+
|
|
21
|
+
## 快速集成
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
### Claude Code
|
|
14
24
|
|
|
15
|
-
|
|
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` | 通用
|
|
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://
|
|
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
|
-
##
|
|
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` |
|
|
20
|
-
| `analytics_query` |
|
|
21
|
-
| `api_request` | 通用
|
|
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
|
-
|
|
29
|
+
### api_request 可访问的 REST 端点
|
|
24
30
|
|
|
25
|
-
| 端点 | 方法 | 用途 |
|
|
26
|
-
|
|
27
|
-
| `/
|
|
28
|
-
| `/api/v1/analytics/
|
|
29
|
-
| `/api/v1/analytics/schema?
|
|
30
|
-
| `/api/v1/analytics/
|
|
31
|
-
| `/
|
|
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` |
|
|
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 →
|
|
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": "
|
|
61
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
77
|
+
if (fromEnv) return normalizeApiUrl(fromEnv);
|
|
72
78
|
const cfg = loadConfig();
|
|
73
|
-
if (cfg.apiUrl) return cfg.apiUrl
|
|
79
|
+
if (cfg.apiUrl) return normalizeApiUrl(cfg.apiUrl);
|
|
74
80
|
return DEFAULT_API_URL;
|
|
75
81
|
}
|
|
76
82
|
|
package/lib/login-server.js
CHANGED
|
@@ -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
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
222
|
-
|
|
223
|
-
res
|
|
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,
|