@wsh19991219/mcp-server 0.1.4 → 0.1.6
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 +44 -24
- package/SKILL.md +2 -2
- package/bin/data-mgr-login.js +21 -0
- package/bin/data-mgr-mcp.js +1 -1
- package/lib/config.js +2 -2
- package/lib/login-page.html +101 -0
- package/lib/login-server.js +24 -117
- package/lib/tools.js +42 -10
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Data Manager MCP Server — 通过 [MCP](https://modelcontextprotocol.io) 协议
|
|
|
4
4
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- **鉴权管理**:浏览器登录(页面填写 API 地址 + 账号密码),JWT 自动保存
|
|
8
8
|
- **表结构探索**:模糊搜表、查看列定义
|
|
9
9
|
- **数据查询**:预定义指标 + 自定义只读 SQL
|
|
10
10
|
- **多库支持**:主库及多个区域数据库 group
|
|
@@ -13,7 +13,7 @@ Data Manager MCP Server — 通过 [MCP](https://modelcontextprotocol.io) 协议
|
|
|
13
13
|
## 安装
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install -g @
|
|
16
|
+
npm install -g @wang19991219/mcp-server
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
需要 **Node.js 18+**。
|
|
@@ -53,29 +53,47 @@ npm install -g @data_mgr/mcp-server
|
|
|
53
53
|
|
|
54
54
|
## MCP 工具
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
56
|
+
|
|
57
|
+
| 工具 | 功能 | 说明 |
|
|
58
|
+
| ----------------- | ---------- | -------------------------------- |
|
|
59
|
+
| `check_auth` | 检查登录状态 | 返回用户名、token 有效期 |
|
|
60
|
+
| `login` | 浏览器登录 | 在页面填写 **API 地址**、账号、密码(无硬编码默认地址) |
|
|
61
|
+
| `analytics_query` | 预定义指标查询 | 支持 metric、filters、limit 等参数 |
|
|
62
|
+
| `api_request` | 通用 REST 请求 | GET/POST/PUT/DELETE,含自定义 SQL |
|
|
63
|
+
|
|
62
64
|
|
|
63
65
|
### api_request 端点
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
| `/
|
|
69
|
-
| `/api/v1/analytics/
|
|
70
|
-
| `/api/v1/analytics/schema?
|
|
71
|
-
| `/api/v1/analytics/
|
|
67
|
+
|
|
68
|
+
| 端点 | 方法 | 用途 |
|
|
69
|
+
| -------------------------------------- | ---- | ------------------ |
|
|
70
|
+
| `/health` | GET | 健康检查,返回可用数据库 group |
|
|
71
|
+
| `/api/v1/analytics/catalog` | GET | 列出可用指标和数据库 group |
|
|
72
|
+
| `/api/v1/analytics/schema?q=<关键词>` | GET | 模糊搜索表名 |
|
|
73
|
+
| `/api/v1/analytics/schema?tables=<表名>` | GET | 查看表的列结构 |
|
|
74
|
+
| `/api/v1/analytics/sql` | POST | 执行自定义只读 SQL |
|
|
75
|
+
|
|
72
76
|
|
|
73
77
|
## 环境变量
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
| `
|
|
79
|
+
|
|
80
|
+
| 变量 | 说明 | 默认值 |
|
|
81
|
+
| --------------------- | ------------------------ | ------------- |
|
|
82
|
+
| `DATA_MGR_API_URL` | 后端 API 地址(可选;未设置时在登录页填写) | — |
|
|
83
|
+
| `DATA_MGR_CONFIG_DIR` | 配置文件目录 | `~/.data-mgr` |
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## 登录说明
|
|
87
|
+
|
|
88
|
+
1. 调用 MCP 工具 `login`(或对话中说「登录 data-mgr」)
|
|
89
|
+
2. 浏览器打开本地登录页,填写:
|
|
90
|
+
- **API 地址**:如 `http://your-host:8092`(8092 端口一般为 HTTP)
|
|
91
|
+
- **账号 / 密码**
|
|
92
|
+
3. 成功后写入 `~/.data-mgr/config.json`(含 `apiUrl` 与 `token`)
|
|
93
|
+
|
|
94
|
+
也可预先设置环境变量 `DATA_MGR_API_URL`,登录页会自动填充。
|
|
95
|
+
|
|
96
|
+
**若登录页仍是旧版(无 API 地址框)**:在 Cursor 中 **Settings → MCP → 重启 data-mgr**,并 **Ctrl+F5** 刷新登录页;或执行 `npm install -g ./data-mgr-mcp-server` 更新全局包。
|
|
79
97
|
|
|
80
98
|
## 使用示例
|
|
81
99
|
|
|
@@ -88,11 +106,13 @@ npm install -g @data_mgr/mcp-server
|
|
|
88
106
|
|
|
89
107
|
## 相关包
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
| [
|
|
109
|
+
|
|
110
|
+
| 包名 | 用途 |
|
|
111
|
+
| ---------------------------------------------------------------------- | --------------------------------- |
|
|
112
|
+
| `[@data_mgr/cli](https://www.npmjs.com/package/@data_mgr/cli)` | Data Manager CLI,适用于 Cursor Agent |
|
|
113
|
+
| `[weather-query-cli](https://www.npmjs.com/package/weather-query-cli)` | 天气查询 CLI |
|
|
114
|
+
|
|
95
115
|
|
|
96
116
|
## License
|
|
97
117
|
|
|
98
|
-
MIT
|
|
118
|
+
MIT
|
package/SKILL.md
CHANGED
|
@@ -15,14 +15,14 @@ description: >-
|
|
|
15
15
|
|
|
16
16
|
1. 全局安装:`npm install -g @data_mgr/mcp-server`
|
|
17
17
|
2. 注册 MCP 服务(见下方 [注册](#注册到-claude-code) 章节)
|
|
18
|
-
3. 对话中触发登录:Agent
|
|
18
|
+
3. 对话中触发登录:Agent 调用 `login`,在浏览器页填写 **API 地址**、账号、密码(无内置默认地址)
|
|
19
19
|
|
|
20
20
|
## MCP 工具
|
|
21
21
|
|
|
22
22
|
| 工具 | 功能 | 关键参数 |
|
|
23
23
|
| --- | --- | --- |
|
|
24
24
|
| `check_auth` | 检查登录状态,返回 username / token 有效期 | 无 |
|
|
25
|
-
| `login` |
|
|
25
|
+
| `login` | 浏览器登录(填写 API 地址 + 账号密码) | `openBrowser`: bool(默认 true) |
|
|
26
26
|
| `analytics_query` | 查询预定义指标 | `metric`(必填), `db`, `params`, `filters`, `limit` |
|
|
27
27
|
| `api_request` | 通用 REST 请求(GET/POST/PUT/DELETE) | `method`(必填), `path`(必填), `body`, `query` |
|
|
28
28
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone login subprocess — always loads latest login-server.js (avoids MCP ESM cache).
|
|
4
|
+
* Usage: node bin/data-mgr-login.js [--no-open]
|
|
5
|
+
* Prints one JSON line to stdout on success.
|
|
6
|
+
*/
|
|
7
|
+
import { runBrowserLogin } from "../lib/login-server.js";
|
|
8
|
+
|
|
9
|
+
const noOpen = process.argv.includes("--no-open");
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const result = await runBrowserLogin({
|
|
13
|
+
openBrowser: !noOpen,
|
|
14
|
+
timeoutMs: 5 * 60 * 1000,
|
|
15
|
+
});
|
|
16
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19
|
+
process.stderr.write(`${message}\n`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
package/bin/data-mgr-mcp.js
CHANGED
package/lib/config.js
CHANGED
|
@@ -37,8 +37,8 @@ export function parseApiUrl(url) {
|
|
|
37
37
|
if (u.protocol !== "http:" && u.protocol !== "https:") {
|
|
38
38
|
throw new Error("仅支持 http 或 https");
|
|
39
39
|
}
|
|
40
|
-
const
|
|
41
|
-
return u.origin +
|
|
40
|
+
const pathSuffix = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
|
|
41
|
+
return u.origin + pathSuffix;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Saved API URL from env or config (no default). */
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Data Manager 登录</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
margin: 0; min-height: 100vh; display: grid; place-items: center;
|
|
11
|
+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
12
|
+
background: linear-gradient(135deg, #0f172a, #1e3a5f);
|
|
13
|
+
color: #e2e8f0;
|
|
14
|
+
}
|
|
15
|
+
.card {
|
|
16
|
+
width: min(420px, 92vw); padding: 2rem; border-radius: 16px;
|
|
17
|
+
background: rgba(15, 23, 42, 0.9); border: 1px solid #334155;
|
|
18
|
+
box-shadow: 0 20px 50px rgba(0,0,0,.35);
|
|
19
|
+
}
|
|
20
|
+
h1 { margin: 0 0 .25rem; font-size: 1.35rem; }
|
|
21
|
+
p { margin: 0 0 1.5rem; color: #94a3b8; font-size: .9rem; }
|
|
22
|
+
label { display: block; margin-bottom: .35rem; font-size: .85rem; color: #cbd5e1; }
|
|
23
|
+
input {
|
|
24
|
+
width: 100%; padding: .65rem .75rem; margin-bottom: 1rem;
|
|
25
|
+
border: 1px solid #475569; border-radius: 8px; background: #0f172a; color: #f8fafc;
|
|
26
|
+
font-size: 1rem;
|
|
27
|
+
}
|
|
28
|
+
input:focus { outline: 2px solid #3b82f6; border-color: #3b82f6; }
|
|
29
|
+
button {
|
|
30
|
+
width: 100%; padding: .75rem; border: none; border-radius: 8px;
|
|
31
|
+
background: #2563eb; color: #fff; font-size: 1rem; font-weight: 600; cursor: pointer;
|
|
32
|
+
}
|
|
33
|
+
button:hover { background: #1d4ed8; }
|
|
34
|
+
button:disabled { opacity: .6; cursor: not-allowed; }
|
|
35
|
+
.msg { margin-top: 1rem; font-size: .9rem; min-height: 1.25rem; }
|
|
36
|
+
.err { color: #f87171; }
|
|
37
|
+
.ok { color: #4ade80; }
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div class="card">
|
|
42
|
+
<h1>Data Manager</h1>
|
|
43
|
+
<p>请输入 API 地址、账号和密码。登录成功后将保存服务器地址与 token。</p>
|
|
44
|
+
<form id="form">
|
|
45
|
+
<label for="apiUrl">API 地址</label>
|
|
46
|
+
<input id="apiUrl" name="apiUrl" type="text" inputmode="url" spellcheck="false"
|
|
47
|
+
placeholder="http://host:8092" required autofocus />
|
|
48
|
+
<label for="username">账号</label>
|
|
49
|
+
<input id="username" name="username" autocomplete="username" required />
|
|
50
|
+
<label for="password">密码</label>
|
|
51
|
+
<input id="password" name="password" type="password" autocomplete="current-password" required />
|
|
52
|
+
<button type="submit" id="btn">登录</button>
|
|
53
|
+
</form>
|
|
54
|
+
<div id="msg" class="msg"></div>
|
|
55
|
+
</div>
|
|
56
|
+
<script>
|
|
57
|
+
const form = document.getElementById('form');
|
|
58
|
+
const msg = document.getElementById('msg');
|
|
59
|
+
const btn = document.getElementById('btn');
|
|
60
|
+
const apiUrlInput = document.getElementById('apiUrl');
|
|
61
|
+
const defaultApiUrl = __DEFAULT_API_URL_JSON__;
|
|
62
|
+
if (defaultApiUrl) apiUrlInput.value = defaultApiUrl;
|
|
63
|
+
else {
|
|
64
|
+
try {
|
|
65
|
+
const last = localStorage.getItem('data-mgr-api-url');
|
|
66
|
+
if (last) apiUrlInput.value = last;
|
|
67
|
+
} catch (_) {}
|
|
68
|
+
}
|
|
69
|
+
form.addEventListener('submit', async (e) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
msg.textContent = '';
|
|
72
|
+
msg.className = 'msg';
|
|
73
|
+
btn.disabled = true;
|
|
74
|
+
btn.textContent = '登录中…';
|
|
75
|
+
const apiUrl = apiUrlInput.value.trim();
|
|
76
|
+
try {
|
|
77
|
+
const res = await fetch('/api/login', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
apiUrl,
|
|
82
|
+
username: document.getElementById('username').value,
|
|
83
|
+
password: document.getElementById('password').value,
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
const data = await res.json().catch(() => ({}));
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
throw new Error(data.message || data.error || ('HTTP ' + res.status));
|
|
89
|
+
}
|
|
90
|
+
try { localStorage.setItem('data-mgr-api-url', apiUrl); } catch (_) {}
|
|
91
|
+
window.location.href = '/success';
|
|
92
|
+
} catch (err) {
|
|
93
|
+
msg.textContent = err.message || '登录失败';
|
|
94
|
+
msg.className = 'msg err';
|
|
95
|
+
btn.disabled = false;
|
|
96
|
+
btn.textContent = '登录';
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
</script>
|
|
100
|
+
</body>
|
|
101
|
+
</html>
|
package/lib/login-server.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
2
5
|
import { randomBytes } from "node:crypto";
|
|
3
6
|
import { spawn } from "node:child_process";
|
|
4
7
|
import { getSavedApiUrl, parseApiUrl, LOGIN_PATH } from "./config.js";
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return LOGIN_PAGE.replace("__DEFAULT_API_URL_JSON__", JSON.stringify(defaultApiUrl ?? ""));
|
|
9
|
-
}
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const LOGIN_PAGE_PATH = path.join(__dirname, "login-page.html");
|
|
10
11
|
|
|
11
12
|
const CORS_HEADERS = {
|
|
12
13
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -14,6 +15,17 @@ const CORS_HEADERS = {
|
|
|
14
15
|
"Access-Control-Allow-Headers": "Content-Type",
|
|
15
16
|
};
|
|
16
17
|
|
|
18
|
+
const NO_CACHE_HEADERS = {
|
|
19
|
+
"Cache-Control": "no-store, no-cache, must-revalidate",
|
|
20
|
+
Pragma: "no-cache",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** @param {string} [defaultApiUrl] */
|
|
24
|
+
function renderLoginPage(defaultApiUrl) {
|
|
25
|
+
const template = fs.readFileSync(LOGIN_PAGE_PATH, "utf8");
|
|
26
|
+
return template.replace("__DEFAULT_API_URL_JSON__", JSON.stringify(defaultApiUrl ?? ""));
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
/** @param {import('node:http').ServerResponse} res @param {number} status @param {Record<string, unknown>} body */
|
|
18
30
|
function sendJson(res, status, body) {
|
|
19
31
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", ...CORS_HEADERS });
|
|
@@ -42,107 +54,6 @@ function formatUpstreamError(err, apiUrl) {
|
|
|
42
54
|
return msg || base.message;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
const LOGIN_PAGE = String.raw`<!DOCTYPE html>
|
|
46
|
-
<html lang="zh-CN">
|
|
47
|
-
<head>
|
|
48
|
-
<meta charset="UTF-8" />
|
|
49
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
50
|
-
<title>Data Manager 登录</title>
|
|
51
|
-
<style>
|
|
52
|
-
* { box-sizing: border-box; }
|
|
53
|
-
body {
|
|
54
|
-
margin: 0; min-height: 100vh; display: grid; place-items: center;
|
|
55
|
-
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
56
|
-
background: linear-gradient(135deg, #0f172a, #1e3a5f);
|
|
57
|
-
color: #e2e8f0;
|
|
58
|
-
}
|
|
59
|
-
.card {
|
|
60
|
-
width: min(400px, 92vw); padding: 2rem; border-radius: 16px;
|
|
61
|
-
background: rgba(15, 23, 42, 0.9); border: 1px solid #334155;
|
|
62
|
-
box-shadow: 0 20px 50px rgba(0,0,0,.35);
|
|
63
|
-
}
|
|
64
|
-
h1 { margin: 0 0 .25rem; font-size: 1.35rem; }
|
|
65
|
-
p { margin: 0 0 1.5rem; color: #94a3b8; font-size: .9rem; }
|
|
66
|
-
label { display: block; margin-bottom: .35rem; font-size: .85rem; color: #cbd5e1; }
|
|
67
|
-
input {
|
|
68
|
-
width: 100%; padding: .65rem .75rem; margin-bottom: 1rem;
|
|
69
|
-
border: 1px solid #475569; border-radius: 8px; background: #0f172a; color: #f8fafc;
|
|
70
|
-
font-size: 1rem;
|
|
71
|
-
}
|
|
72
|
-
input:focus { outline: 2px solid #3b82f6; border-color: #3b82f6; }
|
|
73
|
-
button {
|
|
74
|
-
width: 100%; padding: .75rem; border: none; border-radius: 8px;
|
|
75
|
-
background: #2563eb; color: #fff; font-size: 1rem; font-weight: 600; cursor: pointer;
|
|
76
|
-
}
|
|
77
|
-
button:hover { background: #1d4ed8; }
|
|
78
|
-
button:disabled { opacity: .6; cursor: not-allowed; }
|
|
79
|
-
.msg { margin-top: 1rem; font-size: .9rem; min-height: 1.25rem; }
|
|
80
|
-
.err { color: #f87171; }
|
|
81
|
-
.ok { color: #4ade80; }
|
|
82
|
-
.api { font-size: .75rem; color: #64748b; word-break: break-all; margin-top: 1rem; }
|
|
83
|
-
</style>
|
|
84
|
-
</head>
|
|
85
|
-
<body>
|
|
86
|
-
<div class="card">
|
|
87
|
-
<h1>Data Manager</h1>
|
|
88
|
-
<p>请输入 API 地址、账号和密码。登录成功后将保存服务器地址与 token。</p>
|
|
89
|
-
<form id="form">
|
|
90
|
-
<label for="apiUrl">API 地址</label>
|
|
91
|
-
<input id="apiUrl" name="apiUrl" type="url" autocomplete="url"
|
|
92
|
-
placeholder="http://host:8092" required />
|
|
93
|
-
<label for="username">账号</label>
|
|
94
|
-
<input id="username" name="username" autocomplete="username" required />
|
|
95
|
-
<label for="password">密码</label>
|
|
96
|
-
<input id="password" name="password" type="password" autocomplete="current-password" required />
|
|
97
|
-
<button type="submit" id="btn">登录</button>
|
|
98
|
-
</form>
|
|
99
|
-
<div id="msg" class="msg"></div>
|
|
100
|
-
</div>
|
|
101
|
-
<script>
|
|
102
|
-
const form = document.getElementById('form');
|
|
103
|
-
const msg = document.getElementById('msg');
|
|
104
|
-
const btn = document.getElementById('btn');
|
|
105
|
-
const defaultApiUrl = __DEFAULT_API_URL_JSON__;
|
|
106
|
-
if (defaultApiUrl) form.apiUrl.value = defaultApiUrl;
|
|
107
|
-
else {
|
|
108
|
-
try {
|
|
109
|
-
const last = localStorage.getItem('data-mgr-api-url');
|
|
110
|
-
if (last) form.apiUrl.value = last;
|
|
111
|
-
} catch (_) {}
|
|
112
|
-
}
|
|
113
|
-
form.addEventListener('submit', async (e) => {
|
|
114
|
-
e.preventDefault();
|
|
115
|
-
msg.textContent = '';
|
|
116
|
-
msg.className = 'msg';
|
|
117
|
-
btn.disabled = true;
|
|
118
|
-
btn.textContent = '登录中…';
|
|
119
|
-
try {
|
|
120
|
-
const res = await fetch('/api/login', {
|
|
121
|
-
method: 'POST',
|
|
122
|
-
headers: { 'Content-Type': 'application/json' },
|
|
123
|
-
body: JSON.stringify({
|
|
124
|
-
apiUrl: form.apiUrl.value.trim(),
|
|
125
|
-
username: form.username.value,
|
|
126
|
-
password: form.password.value,
|
|
127
|
-
}),
|
|
128
|
-
});
|
|
129
|
-
const data = await res.json().catch(() => ({}));
|
|
130
|
-
if (!res.ok) {
|
|
131
|
-
throw new Error(data.message || data.error || ('HTTP ' + res.status));
|
|
132
|
-
}
|
|
133
|
-
try { localStorage.setItem('data-mgr-api-url', form.apiUrl.value.trim()); } catch (_) {}
|
|
134
|
-
window.location.href = '/success';
|
|
135
|
-
} catch (err) {
|
|
136
|
-
msg.textContent = err.message || '登录失败';
|
|
137
|
-
msg.className = 'msg err';
|
|
138
|
-
btn.disabled = false;
|
|
139
|
-
btn.textContent = '登录';
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
</script>
|
|
143
|
-
</body>
|
|
144
|
-
</html>`;
|
|
145
|
-
|
|
146
57
|
const SUCCESS_PAGE = String.raw`<!DOCTYPE html>
|
|
147
58
|
<html lang="zh-CN">
|
|
148
59
|
<head>
|
|
@@ -169,19 +80,17 @@ const SUCCESS_PAGE = String.raw`<!DOCTYPE html>
|
|
|
169
80
|
/**
|
|
170
81
|
* @typedef {object} LoginResult
|
|
171
82
|
* @property {string} token
|
|
83
|
+
* @property {string} apiUrl
|
|
172
84
|
* @property {string} [username]
|
|
173
85
|
* @property {number} [userId]
|
|
174
86
|
* @property {number} [tenantId]
|
|
175
87
|
* @property {string} [expiresIn]
|
|
176
88
|
* @property {string} [expiresAt]
|
|
177
89
|
* @property {string} loggedInAt
|
|
178
|
-
* @property {string}
|
|
179
|
-
* @property {string} loginUrl - The URL the user should visit to log in
|
|
90
|
+
* @property {string} loginUrl
|
|
180
91
|
*/
|
|
181
92
|
|
|
182
93
|
/**
|
|
183
|
-
* Start local login server, open browser, return token when login succeeds.
|
|
184
|
-
* Modified for MCP: uses onLoginUrl callback instead of console.log.
|
|
185
94
|
* @param {{ timeoutMs?: number, openBrowser?: boolean, apiUrl?: string, onLoginUrl?: (url: string) => void }} [options]
|
|
186
95
|
* @returns {Promise<LoginResult>}
|
|
187
96
|
*/
|
|
@@ -213,7 +122,10 @@ export function runBrowserLogin(options = {}) {
|
|
|
213
122
|
}
|
|
214
123
|
|
|
215
124
|
if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/login")) {
|
|
216
|
-
res.writeHead(200, {
|
|
125
|
+
res.writeHead(200, {
|
|
126
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
127
|
+
...NO_CACHE_HEADERS,
|
|
128
|
+
});
|
|
217
129
|
res.end(renderLoginPage(defaultApiUrl));
|
|
218
130
|
return;
|
|
219
131
|
}
|
|
@@ -255,14 +167,12 @@ export function runBrowserLogin(options = {}) {
|
|
|
255
167
|
|
|
256
168
|
const data = await loginRes.json().catch(() => ({}));
|
|
257
169
|
|
|
258
|
-
// API returns {code, message, data} — check code for errors
|
|
259
170
|
if (!loginRes.ok || (data.code != null && data.code !== 0)) {
|
|
260
171
|
const message = data.message ?? data.error ?? `Login failed (HTTP ${loginRes.status}, code ${data.code})`;
|
|
261
172
|
sendJson(res, loginRes.ok ? 401 : loginRes.status, { error: message, message });
|
|
262
173
|
return;
|
|
263
174
|
}
|
|
264
175
|
|
|
265
|
-
// Token may be at data.token, data.data.token, or data.data.access_token
|
|
266
176
|
const inner = data.data ?? {};
|
|
267
177
|
const token = data.token ?? inner.token ?? inner.access_token;
|
|
268
178
|
if (!token) {
|
|
@@ -301,7 +211,7 @@ export function runBrowserLogin(options = {}) {
|
|
|
301
211
|
}
|
|
302
212
|
|
|
303
213
|
if (req.method === "GET" && url.pathname === "/success") {
|
|
304
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
214
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", ...NO_CACHE_HEADERS });
|
|
305
215
|
res.end(SUCCESS_PAGE);
|
|
306
216
|
return;
|
|
307
217
|
}
|
|
@@ -319,6 +229,7 @@ export function runBrowserLogin(options = {}) {
|
|
|
319
229
|
cleanup();
|
|
320
230
|
resolve({
|
|
321
231
|
token,
|
|
232
|
+
apiUrl: defaultApiUrl,
|
|
322
233
|
username: url.searchParams.get("username") ?? undefined,
|
|
323
234
|
loggedInAt: new Date().toISOString(),
|
|
324
235
|
loginUrl: `http://127.0.0.1:${server?.address()?.port ?? "?"}/login`,
|
|
@@ -340,8 +251,6 @@ export function runBrowserLogin(options = {}) {
|
|
|
340
251
|
}
|
|
341
252
|
|
|
342
253
|
const loginUrl = `http://127.0.0.1:${addr.port}/login`;
|
|
343
|
-
|
|
344
|
-
// Notify caller of the login URL (replaces console.log for MCP compatibility)
|
|
345
254
|
onLoginUrl(loginUrl);
|
|
346
255
|
|
|
347
256
|
if (openBrowser) {
|
|
@@ -385,9 +294,7 @@ function openUrl(url) {
|
|
|
385
294
|
["-NoProfile", "-Command", `Start-Process -FilePath '${url.replace(/'/g, "''")}'`],
|
|
386
295
|
{ detached: true, stdio: "ignore", windowsHide: true },
|
|
387
296
|
).unref();
|
|
388
|
-
return;
|
|
389
297
|
}
|
|
390
|
-
// Browser open failed - caller already has the URL via onLoginUrl callback
|
|
391
298
|
});
|
|
392
299
|
|
|
393
300
|
child.unref();
|
package/lib/tools.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
1
4
|
import { z } from "zod";
|
|
2
5
|
import {
|
|
3
6
|
loadConfig,
|
|
@@ -9,6 +12,41 @@ import {
|
|
|
9
12
|
} from "./config.js";
|
|
10
13
|
import { apiRequest, buildQuery } from "./api.js";
|
|
11
14
|
|
|
15
|
+
const LOGIN_SCRIPT = path.join(
|
|
16
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
17
|
+
"..",
|
|
18
|
+
"bin",
|
|
19
|
+
"data-mgr-login.js",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/** @param {boolean} openBrowser */
|
|
23
|
+
function runLoginSubprocess(openBrowser) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const args = [LOGIN_SCRIPT];
|
|
26
|
+
if (!openBrowser) args.push("--no-open");
|
|
27
|
+
const child = spawn(process.execPath, args, {
|
|
28
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
});
|
|
31
|
+
let stdout = "";
|
|
32
|
+
let stderr = "";
|
|
33
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
34
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
35
|
+
child.on("error", reject);
|
|
36
|
+
child.on("close", (code) => {
|
|
37
|
+
if (code !== 0) {
|
|
38
|
+
reject(new Error(stderr.trim() || `Login exited with code ${code}`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
resolve(JSON.parse(stdout.trim()));
|
|
43
|
+
} catch {
|
|
44
|
+
reject(new Error("Invalid login response from subprocess"));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
12
50
|
/**
|
|
13
51
|
* Register all data-mgr tools on the MCP server.
|
|
14
52
|
* @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server
|
|
@@ -165,21 +203,14 @@ export function registerTools(server) {
|
|
|
165
203
|
// Tool 4: login — trigger browser login flow
|
|
166
204
|
server.tool(
|
|
167
205
|
"login",
|
|
168
|
-
"Trigger
|
|
206
|
+
"Trigger browser login. User enters API base URL, username, and password on the local login page; apiUrl and token are saved to ~/.data-mgr/config.json. Returns login URL if the browser cannot open.",
|
|
169
207
|
{
|
|
170
208
|
openBrowser: z.boolean().optional().describe("Whether to open browser automatically (default true)"),
|
|
171
209
|
},
|
|
172
210
|
async ({ openBrowser }) => {
|
|
173
211
|
try {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
// Collect login URL from the callback
|
|
177
|
-
let loginUrl = "";
|
|
178
|
-
const result = await runBrowserLogin({
|
|
179
|
-
openBrowser: openBrowser !== false,
|
|
180
|
-
timeoutMs: 5 * 60 * 1000,
|
|
181
|
-
onLoginUrl: (url) => { loginUrl = url; },
|
|
182
|
-
});
|
|
212
|
+
const result = await runLoginSubprocess(openBrowser !== false);
|
|
213
|
+
const loginUrl = result.loginUrl;
|
|
183
214
|
|
|
184
215
|
const expiresAt = computeExpiresAt(result.expiresIn, new Date(result.loggedInAt));
|
|
185
216
|
|
|
@@ -195,6 +226,7 @@ export function registerTools(server) {
|
|
|
195
226
|
});
|
|
196
227
|
|
|
197
228
|
const lines = [
|
|
229
|
+
`API URL: ${result.apiUrl}`,
|
|
198
230
|
`Logged in as: ${result.username ?? "(unknown)"}`,
|
|
199
231
|
`User ID: ${result.userId}`,
|
|
200
232
|
`Tenant ID: ${result.tenantId}`,
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wsh19991219/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "MCP server for Data Manager API — stdio transport, JWT auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/tools.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"data-mgr-mcp": "bin/data-mgr-mcp.js"
|
|
8
|
+
"data-mgr-mcp": "bin/data-mgr-mcp.js",
|
|
9
|
+
"data-mgr-login": "bin/data-mgr-login.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"bin",
|