@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 CHANGED
@@ -4,7 +4,7 @@ Data Manager MCP Server — 通过 [MCP](https://modelcontextprotocol.io) 协议
4
4
 
5
5
  ## 功能
6
6
 
7
- - **鉴权管理**:浏览器一键登录,JWT 自动续期
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 @data_mgr/mcp-server
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
- | `check_auth` | 检查登录状态 | 返回用户名、token 有效期 |
59
- | `login` | 浏览器登录 | 打开浏览器完成 OAuth 登录 |
60
- | `analytics_query` | 预定义指标查询 | 支持 metric、filters、limit 等参数 |
61
- | `api_request` | 通用 REST 请求 | GET/POST/PUT/DELETE,含自定义 SQL |
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
- | `/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 |
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
- | `DATA_MGR_API_URL` | 后端 API 地址(可选;未设置时在登录页填写) | |
78
- | `DATA_MGR_CONFIG_DIR` | 配置文件目录 | `~/.data-mgr` |
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
- | [`@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 |
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 会自动调用 `login` 工具打开浏览器
18
+ 3. 对话中触发登录:Agent 调用 `login`,在浏览器页填写 **API 地址**、账号、密码(无内置默认地址)
19
19
 
20
20
  ## MCP 工具
21
21
 
22
22
  | 工具 | 功能 | 关键参数 |
23
23
  | --- | --- | --- |
24
24
  | `check_auth` | 检查登录状态,返回 username / token 有效期 | 无 |
25
- | `login` | 浏览器登录获取 JWT | `openBrowser`: bool(默认 true) |
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
+ }
@@ -6,7 +6,7 @@ import { registerTools } from "../lib/tools.js";
6
6
 
7
7
  const server = new McpServer({
8
8
  name: "data-mgr",
9
- version: "0.1.0",
9
+ version: "0.1.6",
10
10
  });
11
11
 
12
12
  registerTools(server);
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 path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
41
- return u.origin + path;
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>
@@ -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
- /** @param {string} [defaultApiUrl] */
7
- function renderLoginPage(defaultApiUrl) {
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} apiUrl
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, { "Content-Type": "text/html; charset=utf-8" });
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 the data-mgr browser login flow. Opens a browser window for the user to enter credentials. Returns login result on success. If browser cannot be opened, returns the URL for manual visit.",
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 { runBrowserLogin } = await import("./login-server.js");
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.4",
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",